Initial support for Hand Raise feature

Signed-off-by: Milton Moura <miltonmoura@gmail.com>
This commit is contained in:
Milton Moura
2024-08-07 01:58:14 +00:00
parent cec7fc8f5b
commit 48cf487e0a
8 changed files with 192 additions and 8 deletions

View File

@@ -54,6 +54,7 @@
"options": "Options",
"password": "Password",
"profile": "Profile",
"raise_hand": "Raise hand",
"settings": "Settings",
"unencrypted": "Not encrypted",
"username": "Username",

View File

@@ -18,6 +18,7 @@ import {
SettingsSolidIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import RaiseHandIcon from "../icons/RaiseHand.svg?react";
import styles from "./Button.module.css";
interface MicButtonProps extends ComponentPropsWithoutRef<"button"> {
@@ -91,6 +92,27 @@ export const ShareScreenButton: FC<ShareScreenButtonProps> = ({
);
};
interface RaiseHandButtonProps extends ComponentPropsWithoutRef<"button"> {
raised: boolean;
}
export const RaiseHandButton: FC<RaiseHandButtonProps> = ({
raised,
...props
}) => {
const { t } = useTranslation();
return (
<Tooltip label={t("common.raise_hand")}>
<CpdButton
iconOnly
Icon={RaiseHandIcon}
kind={raised ? "primary" : "secondary"}
{...props}
/>
</Tooltip>
);
};
export const EndCallButton: FC<ComponentPropsWithoutRef<"button">> = ({
className,
...props

9
src/icons/RaiseHand.svg Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="14px" height="20px" viewBox="0 0 14 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>raise-hand</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="raise-hand" fill="#FFFFFF">
<path d="M11.5863636,2.42414773 C11.4352273,2.57556818 11.3409091,2.78522727 11.3409091,3.01534091 L11.3409091,7.65909091 C11.3409091,7.79176136 11.2329545,7.89971591 11.1002841,7.89971591 C10.9676136,7.89971591 10.8596591,7.79176136 10.8596591,7.65909091 L10.8596591,1.93892045 C10.8596591,1.69517045 10.7653409,1.49034091 10.6136364,1.34630682 C10.4519886,1.19289773 10.221875,1.10369318 9.96619318,1.10369318 C9.74232955,1.10369318 9.53778409,1.19289773 9.38181818,1.34318182 C9.225,1.50028409 9.13068182,1.709375 9.13068182,1.93892045 L9.13068182,7.40028409 C9.13068182,7.53295455 9.02272727,7.64090909 8.89005682,7.64090909 C8.75738636,7.64090909 8.64943182,7.53295455 8.64943182,7.40028409 L8.64943182,0.863068182 C8.64943182,0.619318182 8.55511364,0.414204545 8.40340909,0.270170455 C8.24176136,0.116761364 8.01164773,0.0275568182 7.75596591,0.0275568182 C7.52670455,0.0275568182 7.31761364,0.121875 7.16619318,0.273295455 C7.01505682,0.424431818 6.92073864,0.633522727 6.92073864,0.863068182 L6.92073864,7.65909091 C6.92073864,7.79176136 6.81278409,7.89971591 6.68011364,7.89971591 C6.54744318,7.89971591 6.43948864,7.79176136 6.43948864,7.65909091 L6.43948864,1.93892045 C6.43948864,1.69431818 6.34573864,1.48948864 6.19403409,1.34573864 C6.03238636,1.19261364 5.80227273,1.10369318 5.54602273,1.10369318 C5.32215909,1.10369318 5.11789773,1.19289773 4.96193182,1.34318182 C4.80511364,1.50028409 4.71079545,1.709375 4.71079545,1.93892045 L4.71079545,8.73551136 C4.71505682,9.01221591 4.65568182,9.25198864 4.54318182,9.45511364 C4.41761364,9.67102273 4.24289773,9.82017045 4.034375,9.90227273 C3.83125,9.98210227 3.59744318,9.996875 3.35539773,9.93664773 C3.08806818,9.87017045 2.80852273,9.71278409 2.54801136,9.45227273 L1.48892045,8.39318182 C1.32840909,8.23210227 1.11590909,8.15198864 0.903693182,8.15198864 C0.691477273,8.15198864 0.478977273,8.23267045 0.31875,8.39289773 L0.238920455,8.47329545 C0.09375,8.63267045 0.0184659091,8.83863636 0.0173160196,9.04147727 C0.0161931818,9.24005682 0.0866477273,9.4375 0.233238636,9.58409091 C1.38806818,10.7235795 2.74090909,11.8667614 3.95255682,12.96875 C4.76960227,13.7178977 4.79090909,14.1713068 4.75823864,15.6471591 L11.9457386,15.6471591 L12.0065341,15.6482955 C12.1059659,13.5639205 12.3755682,12.6994318 12.6201705,11.9136364 C12.85625,11.1556818 13.0693182,10.4724432 13.0693182,8.73551136 L13.0693182,3.01534091 C13.0693182,2.77130682 12.975,2.56647727 12.8232955,2.42244318 C12.6616477,2.26875 12.43125,2.17954545 12.1758523,2.17954545 C11.9460227,2.17954545 11.7366477,2.27357955 11.5863636,2.42414773 L11.5863636,2.42414773 Z M11.9457386,16.1284091 L4.76960227,16.1284091 C4.46022727,16.1284091 4.17897727,16.2548295 3.97528409,16.4579545 C3.77215909,16.6616477 3.64573864,16.9431818 3.64573864,17.2522727 L3.64573864,19.9911932 L13.0696023,19.9911932 L13.0696023,17.2522727 C13.0696023,16.9431818 12.9431818,16.6616477 12.7400568,16.4579545 C12.5363636,16.2548295 12.2551136,16.1284091 11.9457386,16.1284091 L11.9457386,16.1284091 Z" id="Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -10,7 +10,14 @@ import {
RoomContext,
useLocalParticipant,
} from "@livekit/components-react";
import { ConnectionState, Room } from "livekit-client";
import {
ConnectionState,
// eslint-disable-next-line camelcase
DataPacket_Kind,
Participant,
Room,
RoomEvent,
} from "livekit-client";
import { MatrixClient } from "matrix-js-sdk/src/client";
import {
FC,
@@ -39,6 +46,7 @@ import {
MicButton,
VideoButton,
ShareScreenButton,
RaiseHandButton,
SettingsButton,
} from "../button";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
@@ -78,6 +86,7 @@ import { makeOneOnOneLayout } from "../grid/OneOnOneLayout";
import { makeSpotlightExpandedLayout } from "../grid/SpotlightExpandedLayout";
import { makeSpotlightLandscapeLayout } from "../grid/SpotlightLandscapeLayout";
import { makeSpotlightPortraitLayout } from "../grid/SpotlightPortraitLayout";
import { RaisedHandsProvider, useRaisedHands } from "./useRaisedHands";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
@@ -130,12 +139,14 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
return (
<RoomContext.Provider value={livekitRoom}>
<InCallView
{...props}
vm={vm}
livekitRoom={livekitRoom}
connState={connState}
/>
<RaisedHandsProvider>
<InCallView
{...props}
vm={vm}
livekitRoom={livekitRoom}
connState={connState}
/>
</RaisedHandsProvider>
</RoomContext.Provider>
);
};
@@ -298,6 +309,34 @@ export const InCallView: FC<InCallViewProps> = ({
[vm],
);
const { raisedHands, setRaisedHands } = useRaisedHands();
const isHandRaised = raisedHands.includes(
localParticipant.identity.split(":")[0] +
":" +
localParticipant.identity.split(":")[1],
);
useEffect(() => {
const handleDataReceived = (
payload: Uint8Array,
participant?: Participant,
// eslint-disable-next-line camelcase
kind?: DataPacket_Kind,
): void => {
const decoder = new TextDecoder();
const strData = decoder.decode(payload);
// get json object from strData
const data = JSON.parse(strData);
setRaisedHands(data.raisedHands);
};
livekitRoom.on(RoomEvent.DataReceived, handleDataReceived);
return (): void => {
livekitRoom.off(RoomEvent.DataReceived, handleDataReceived);
};
}, [livekitRoom, setRaisedHands]);
useEffect(() => {
widget?.api.transport
.send(
@@ -479,6 +518,37 @@ export const InCallView: FC<InCallViewProps> = ({
.catch(logger.error);
}, [localParticipant, isScreenShareEnabled]);
const toggleRaisedHand = useCallback(() => {
// TODO: wtf
const userId =
localParticipant.identity.split(":")[0] +
":" +
localParticipant.identity.split(":")[1];
const raisedHand = raisedHands.includes(userId);
let result = raisedHands;
if (raisedHand) {
result = raisedHands.filter((id) => id !== userId);
} else {
result = [...raisedHands, userId];
}
try {
const strData = JSON.stringify({
raisedHands: result,
});
const encoder = new TextEncoder();
const data = encoder.encode(strData);
livekitRoom.localParticipant.publishData(data, { reliable: true });
setRaisedHands(result);
} catch (e) {
logger.error(e);
}
}, [
livekitRoom.localParticipant,
localParticipant.identity,
raisedHands,
setRaisedHands,
]);
let footer: JSX.Element | null;
if (noControls) {
@@ -513,7 +583,14 @@ export const InCallView: FC<InCallViewProps> = ({
/>,
);
}
buttons.push(<SettingsButton key="4" onClick={openSettings} />);
buttons.push(
<RaiseHandButton
key="4"
onClick={toggleRaisedHand}
raised={isHandRaised}
/>,
);
buttons.push(<SettingsButton key="5" onClick={openSettings} />);
}
buttons.push(

View File

@@ -0,0 +1,47 @@
/*
Copyright 2024 Milton Moura <miltonmoura@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createContext, useContext, useState, ReactNode } from "react";
interface RaisedHandsContextType {
raisedHands: string[];
setRaisedHands: React.Dispatch<React.SetStateAction<string[]>>;
}
const RaisedHandsContext = createContext<RaisedHandsContextType | undefined>(
undefined,
);
export const useRaisedHands = (): RaisedHandsContextType => {
const context = useContext(RaisedHandsContext);
if (!context) {
throw new Error("useRaisedHands must be used within a RaisedHandsProvider");
}
return context;
};
export const RaisedHandsProvider = ({
children,
}: {
children: ReactNode;
}): JSX.Element => {
const [raisedHands, setRaisedHands] = useState<string[]>([]);
return (
<RaisedHandsContext.Provider value={{ raisedHands, setRaisedHands }}>
{children}
</RaisedHandsContext.Provider>
);
};

View File

@@ -44,6 +44,7 @@ import {
import { Slider } from "../Slider";
import { MediaView } from "./MediaView";
import { useLatest } from "../useLatest";
import { useRaisedHands } from "../room/useRaisedHands";
interface TileProps {
className?: string;
@@ -90,6 +91,8 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
},
[vm],
);
const { raisedHands } = useRaisedHands();
const raisedHand = raisedHands.includes(vm.member?.userId ?? "");
const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon;
@@ -144,6 +147,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
{menu}
</Menu>
}
raisedHand={raisedHand}
{...props}
/>
);

View File

@@ -90,6 +90,22 @@ unconditionally select the container so we can use cqmin units */
place-items: start;
}
.raisedHand {
margin: var(--cpd-space-2x);
padding: var(--cpd-space-2x);
padding-block: var(--cpd-space-2x);
color: var(--cpd-color-icon-secondary);
background-color: var(--cpd-color-icon-secondary);
display: flex;
align-items: center;
border-radius: var(--cpd-radius-pill-effect);
user-select: none;
overflow: hidden;
box-shadow: var(--small-drop-shadow);
box-sizing: border-box;
max-inline-size: 100%;
}
.nameTag {
grid-area: nameTag;
padding: var(--cpd-space-1x);

View File

@@ -16,6 +16,7 @@ import { Text, Tooltip } from "@vector-im/compound-web";
import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import styles from "./MediaView.module.css";
import RaiseHandIcon from "../icons/RaiseHand.svg?react";
import { Avatar } from "../Avatar";
interface Props extends ComponentProps<typeof animated.div> {
@@ -32,6 +33,7 @@ interface Props extends ComponentProps<typeof animated.div> {
nameTagLeadingIcon?: ReactNode;
displayName: string;
primaryButton?: ReactNode;
raisedHand: boolean;
}
export const MediaView = forwardRef<HTMLDivElement, Props>(
@@ -50,6 +52,7 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
nameTagLeadingIcon,
displayName,
primaryButton,
raisedHand,
...props
},
ref,
@@ -86,6 +89,11 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
)}
</div>
<div className={styles.fg}>
{raisedHand && (
<div className={styles.raisedHand}>
<RaiseHandIcon width={22} height={22} />
</div>
)}
<div className={styles.nameTag}>
{nameTagLeadingIcon}
<Text as="span" size="sm" weight="medium" className={styles.name}>