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:
Milton Moura
2024-11-04 08:54:13 -01:00
committed by GitHub
parent f2ed07c258
commit 1897210a60
24 changed files with 1149 additions and 30 deletions

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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}
/>
);

View File

@@ -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}>