Skip to content

Commit

Permalink
Initial support for Hand Raise feature
Browse files Browse the repository at this point in the history
Signed-off-by: Milton Moura <[email protected]>
  • Loading branch information
mgcm committed Sep 4, 2024
1 parent cc813fd commit 82dc581
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 8 deletions.
1 change: 1 addition & 0 deletions public/locales/en-GB/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"options": "Options",
"password": "Password",
"profile": "Profile",
"raise_hand": "Raise hand",
"settings": "Settings",
"unencrypted": "Not encrypted",
"username": "Username",
Expand Down
22 changes: 22 additions & 0 deletions src/button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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"> {
Expand Down Expand Up @@ -100,6 +101,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
Expand Down
9 changes: 9 additions & 0 deletions src/icons/RaiseHand.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 86 additions & 8 deletions src/room/InCallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,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,
Expand All @@ -38,6 +45,7 @@ import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import classNames from "classnames";
import { BehaviorSubject, of } from "rxjs";
import { useObservableEagerState } from "observable-hooks";
import { logger } from "matrix-js-sdk/src/logger";

import LogoMark from "../icons/LogoMark.svg?react";
import LogoType from "../icons/LogoType.svg?react";
Expand All @@ -47,6 +55,7 @@ import {
MicButton,
VideoButton,
ShareScreenButton,
RaiseHandButton,
SettingsButton,
} from "../button";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
Expand Down Expand Up @@ -86,6 +95,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 ?? {});

Expand Down Expand Up @@ -136,12 +146,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>
);
};
Expand Down Expand Up @@ -304,6 +316,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(
gridMode === "grid"
Expand Down Expand Up @@ -479,6 +519,37 @@ export const InCallView: FC<InCallViewProps> = ({
});
}, [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) {
Expand Down Expand Up @@ -513,7 +584,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(
Expand Down
47 changes: 47 additions & 0 deletions src/room/useRaisedHands.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
Copyright 2024 Milton Moura <[email protected]>
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>
);
};
4 changes: 4 additions & 0 deletions src/tile/GridTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
import { Slider } from "../Slider";
import { MediaView } from "./MediaView";
import { useLatest } from "../useLatest";
import { useRaisedHands } from "../room/useRaisedHands";

interface TileProps {
className?: string;
Expand Down Expand Up @@ -99,6 +100,8 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
},
[vm],
);
const { raisedHands } = useRaisedHands();
const raisedHand = raisedHands.includes(vm.member?.userId ?? "");

const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon;

Expand Down Expand Up @@ -153,6 +156,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
{menu}
</Menu>
}
raisedHand={raisedHand}
{...props}
/>
);
Expand Down
16 changes: 16 additions & 0 deletions src/tile/MediaView.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,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);
Expand Down
8 changes: 8 additions & 0 deletions src/tile/MediaView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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> {
Expand All @@ -41,6 +42,7 @@ interface Props extends ComponentProps<typeof animated.div> {
nameTagLeadingIcon?: ReactNode;
displayName: string;
primaryButton?: ReactNode;
raisedHand: boolean;
}

export const MediaView = forwardRef<HTMLDivElement, Props>(
Expand All @@ -59,6 +61,7 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
nameTagLeadingIcon,
displayName,
primaryButton,
raisedHand,
...props
},
ref,
Expand Down Expand Up @@ -95,6 +98,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}>
Expand Down

0 comments on commit 82dc581

Please sign in to comment.