Hand raise feature (#2542)
* Initial support for Hand Raise feature Signed-off-by: Milton Moura <miltonmoura@gmail.com> * Refactored to use reaction and redaction events Signed-off-by: Milton Moura <miltonmoura@gmail.com> * Replacing button svg with raised hand emoji Signed-off-by: Milton Moura <miltonmoura@gmail.com> * SpotlightTile should not duplicate the raised hand Signed-off-by: Milton Moura <miltonmoura@gmail.com> * Update src/room/useRaisedHands.tsx Element Call recently changed to AGPL-3.0 * Use relations to load existing reactions when joining the call Signed-off-by: Milton Moura <miltonmoura@gmail.com> * Links to sha commit of matrix-js-sdk that exposes the call membership event id and refactors some async code Signed-off-by: Milton Moura <miltonmoura@gmail.com> * Removing RaiseHand.svg * Check for reaction & redaction capabilities in widget mode Signed-off-by: Milton Moura <miltonmoura@gmail.com> * Fix failing GridTile test Signed-off-by: Milton Moura <miltonmoura@gmail.com> * Center align hand raise. * Add support for displaying the duration of a raised hand. * Add a sound for when a hand is raised. * Refactor raised hand indicator and add tests. * lint * Refactor into own files. * Redact the right thing. * Tidy up useEffect * Lint tests * Remove extra layer * Add better sound. (woosh) * Add a small mode for spotlight * Fix timestamp calculation on relaod. * Fix call border resizing video * lint * Fix and update tests * Allow timer to be configurable. * Add preferences tab for choosing to enable timer. * Drop border from raised hand icon * Handle cases when a new member event happens. * Prevent infinite loop * Major refactor to support various state problems. * Tidy up and finish test rewrites * Add some explanation comments. * Even more comments. * Use proper duration formatter * Remove rerender * Fix redactions not working because they pick up events in transit. * More tidying * Use deferred value * linting * Add tests for cases where we got a reaction from someone else. * Be even less brittle. * Transpose border to GridTile. * lint --------- Signed-off-by: Milton Moura <miltonmoura@gmail.com> Co-authored-by: fkwp <fkwp@users.noreply.github.com> Co-authored-by: Half-Shot <will@half-shot.uk> Co-authored-by: Will Hunt <github@half-shot.uk>
This commit is contained in:
@@ -21,6 +21,15 @@ borders don't support gradients */
|
||||
transition: opacity ease 0.15s;
|
||||
inset: calc(-1 * var(--cpd-border-width-4));
|
||||
border-radius: var(--cpd-space-5x);
|
||||
background-blend-mode: overlay, normal;
|
||||
}
|
||||
|
||||
.tile.speaking {
|
||||
/* !important because speaking border should take priority over hover */
|
||||
outline: var(--cpd-border-width-1) solid var(--cpd-color-bg-canvas-default) !important;
|
||||
}
|
||||
|
||||
.tile.speaking::before {
|
||||
background: linear-gradient(
|
||||
119deg,
|
||||
rgba(13, 92, 189, 0.7) 0%,
|
||||
@@ -31,15 +40,25 @@ borders don't support gradients */
|
||||
rgba(13, 92, 189, 0.9) 0%,
|
||||
rgba(13, 189, 168, 0.9) 100%
|
||||
);
|
||||
background-blend-mode: overlay, normal;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tile.speaking {
|
||||
/* !important because speaking border should take priority over hover */
|
||||
outline: var(--cpd-border-width-1) solid var(--cpd-color-bg-canvas-default) !important;
|
||||
.tile.handRaised {
|
||||
/* !important because hand raised border should take priority over hover */
|
||||
outline: var(--cpd-border-width-2) solid var(--cpd-color-bg-canvas-default) !important;
|
||||
}
|
||||
|
||||
.tile.speaking::before {
|
||||
.tile.handRaised::before {
|
||||
background: linear-gradient(
|
||||
119deg,
|
||||
var(--cpd-color-yellow-1200) 0%,
|
||||
var(--cpd-color-yellow-900) 100%
|
||||
),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
var(--cpd-color-yellow-1200) 0%,
|
||||
var(--cpd-color-yellow-900) 100%
|
||||
);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ import { RemoteTrackPublication } from "livekit-client";
|
||||
import { test, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { axe } from "vitest-axe";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
|
||||
import { GridTile } from "./GridTile";
|
||||
import { withRemoteMedia } from "../utils/test";
|
||||
import { ReactionsProvider } from "../useReactions";
|
||||
|
||||
test("GridTile is accessible", async () => {
|
||||
await withRemoteMedia(
|
||||
@@ -25,15 +27,29 @@ test("GridTile is accessible", async () => {
|
||||
({}) as Partial<RemoteTrackPublication> as RemoteTrackPublication,
|
||||
},
|
||||
async (vm) => {
|
||||
const fakeRtcSession = {
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
room: {
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
client: {
|
||||
getUserId: () => null,
|
||||
},
|
||||
},
|
||||
memberships: [],
|
||||
} as unknown as MatrixRTCSession;
|
||||
const { container } = render(
|
||||
<GridTile
|
||||
vm={vm}
|
||||
onOpenProfile={() => {}}
|
||||
targetWidth={300}
|
||||
targetHeight={200}
|
||||
showVideo
|
||||
showSpeakingIndicators
|
||||
/>,
|
||||
<ReactionsProvider rtcSession={fakeRtcSession}>
|
||||
<GridTile
|
||||
vm={vm}
|
||||
onOpenProfile={() => {}}
|
||||
targetWidth={300}
|
||||
targetHeight={200}
|
||||
showVideo
|
||||
showSpeakingIndicators
|
||||
/>
|
||||
</ReactionsProvider>,
|
||||
);
|
||||
expect(await axe(container)).toHaveNoViolations();
|
||||
// Name should be visible
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
import { Slider } from "../Slider";
|
||||
import { MediaView } from "./MediaView";
|
||||
import { useLatest } from "../useLatest";
|
||||
import { useReactions } from "../useReactions";
|
||||
|
||||
interface TileProps {
|
||||
className?: string;
|
||||
@@ -90,6 +91,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
||||
},
|
||||
[vm],
|
||||
);
|
||||
const { raisedHands } = useReactions();
|
||||
|
||||
const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon;
|
||||
|
||||
@@ -107,6 +109,10 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
||||
</>
|
||||
);
|
||||
|
||||
const handRaised: Date | undefined = raisedHands[vm.member?.userId ?? ""];
|
||||
|
||||
const showSpeaking = showSpeakingIndicators && speaking;
|
||||
|
||||
const tile = (
|
||||
<MediaView
|
||||
ref={ref}
|
||||
@@ -116,7 +122,8 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
||||
videoEnabled={videoEnabled && showVideo}
|
||||
videoFit={cropVideo ? "cover" : "contain"}
|
||||
className={classNames(className, styles.tile, {
|
||||
[styles.speaking]: showSpeakingIndicators && speaking,
|
||||
[styles.speaking]: showSpeaking,
|
||||
[styles.handRaised]: !showSpeaking && !!handRaised,
|
||||
})}
|
||||
nameTagLeadingIcon={
|
||||
<MicIcon
|
||||
@@ -144,6 +151,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
||||
{menu}
|
||||
</Menu>
|
||||
}
|
||||
raisedHandTime={handRaised}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,8 @@ import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import styles from "./MediaView.module.css";
|
||||
import { Avatar } from "../Avatar";
|
||||
import { RaisedHandIndicator } from "../reactions/RaisedHandIndicator";
|
||||
import { showHandRaisedTimer, useSetting } from "../settings/settings";
|
||||
|
||||
interface Props extends ComponentProps<typeof animated.div> {
|
||||
className?: string;
|
||||
@@ -32,6 +34,7 @@ interface Props extends ComponentProps<typeof animated.div> {
|
||||
nameTagLeadingIcon?: ReactNode;
|
||||
displayName: string;
|
||||
primaryButton?: ReactNode;
|
||||
raisedHandTime?: Date;
|
||||
}
|
||||
|
||||
export const MediaView = forwardRef<HTMLDivElement, Props>(
|
||||
@@ -50,11 +53,15 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
|
||||
nameTagLeadingIcon,
|
||||
displayName,
|
||||
primaryButton,
|
||||
raisedHandTime,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const [handRaiseTimerVisible] = useSetting(showHandRaisedTimer);
|
||||
|
||||
const avatarSize = Math.round(Math.min(targetWidth, targetHeight) / 2);
|
||||
|
||||
return (
|
||||
<animated.div
|
||||
@@ -72,7 +79,7 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
|
||||
<Avatar
|
||||
id={member?.userId ?? displayName}
|
||||
name={displayName}
|
||||
size={Math.round(Math.min(targetWidth, targetHeight) / 2)}
|
||||
size={avatarSize}
|
||||
src={member?.getMxcAvatarUrl()}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
@@ -86,6 +93,11 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.fg}>
|
||||
<RaisedHandIndicator
|
||||
raisedHandTime={raisedHandTime}
|
||||
minature={avatarSize < 96}
|
||||
showTimer={handRaiseTimerVisible}
|
||||
/>
|
||||
<div className={styles.nameTag}>
|
||||
{nameTagLeadingIcon}
|
||||
<Text as="span" size="sm" weight="medium" className={styles.name}>
|
||||
|
||||
Reference in New Issue
Block a user