From bc0ab92394550c4d38fd24f2f32acafe61cfb087 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Wed, 6 Nov 2024 11:00:19 +0000 Subject: [PATCH] Add feature to release hand raised when the tile indicator is clicked. (#2721) * Refactor to add support for lowering hand on indicator click. * Cleanup and lint. * fix icon being a little off --- src/button/RaisedHandToggleButton.tsx | 13 +--- src/reactions/RaisedHandIndicator.module.css | 9 ++- src/reactions/RaisedHandIndicator.test.tsx | 12 ++++ src/reactions/RaisedHandIndicator.tsx | 65 +++++++++++++++----- src/tile/GridTile.tsx | 5 +- src/tile/MediaView.tsx | 3 + src/useReactions.test.tsx | 12 +--- src/useReactions.tsx | 36 ++++++++--- 8 files changed, 106 insertions(+), 49 deletions(-) diff --git a/src/button/RaisedHandToggleButton.tsx b/src/button/RaisedHandToggleButton.tsx index 277817def..42006f6ab 100644 --- a/src/button/RaisedHandToggleButton.tsx +++ b/src/button/RaisedHandToggleButton.tsx @@ -62,7 +62,7 @@ export function RaiseHandToggleButton({ client, rtcSession, }: RaisedHandToggleButtonProps): ReactNode { - const { raisedHands, myReactionId } = useReactions(); + const { raisedHands, lowerHand } = useReactions(); const [busy, setBusy] = useState(false); const userId = client.getUserId()!; const isHandRaised = !!raisedHands[userId]; @@ -71,16 +71,9 @@ export function RaiseHandToggleButton({ const toggleRaisedHand = useCallback(() => { const raiseHand = async (): Promise => { if (isHandRaised) { - if (!myReactionId) { - logger.warn(`Hand raised but no reaction event to redact!`); - return; - } try { setBusy(true); - await client.redactEvent(rtcSession.room.roomId, myReactionId); - logger.debug("Redacted raise hand event"); - } catch (ex) { - logger.error("Failed to redact reaction event", myReactionId, ex); + await lowerHand(); } finally { setBusy(false); } @@ -118,9 +111,9 @@ export function RaiseHandToggleButton({ client, isHandRaised, memberships, - myReactionId, rtcSession.room.roomId, userId, + lowerHand, ]); return ( diff --git a/src/reactions/RaisedHandIndicator.module.css b/src/reactions/RaisedHandIndicator.module.css index 4c274374f..563527a2a 100644 --- a/src/reactions/RaisedHandIndicator.module.css +++ b/src/reactions/RaisedHandIndicator.module.css @@ -5,6 +5,11 @@ color: var(--cpd-color-icon-secondary); } +.button { + display: contents; + background: none; +} + .raisedHandWidget > p { padding: none; margin-top: auto; @@ -42,11 +47,11 @@ height: var(--cpd-space-6x); display: inline-block; text-align: center; - font-size: 16px; + font-size: 1.3em; } .raisedHandLarge > span { width: var(--cpd-space-8x); height: var(--cpd-space-8x); - font-size: 22px; + font-size: 1.9em; } diff --git a/src/reactions/RaisedHandIndicator.test.tsx b/src/reactions/RaisedHandIndicator.test.tsx index 22a665a72..fb728b483 100644 --- a/src/reactions/RaisedHandIndicator.test.tsx +++ b/src/reactions/RaisedHandIndicator.test.tsx @@ -40,4 +40,16 @@ describe("RaisedHandIndicator", () => { ); expect(container.firstChild).toMatchSnapshot(); }); + test("can be clicked", () => { + const dateTime = new Date(); + let wasClicked = false; + const { getByRole } = render( + (wasClicked = true)} + />, + ); + getByRole("button").click(); + expect(wasClicked).toBe(true); + }); }); diff --git a/src/reactions/RaisedHandIndicator.tsx b/src/reactions/RaisedHandIndicator.tsx index 19ddaf46b..681e6d266 100644 --- a/src/reactions/RaisedHandIndicator.tsx +++ b/src/reactions/RaisedHandIndicator.tsx @@ -5,7 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ReactNode, useEffect, useState } from "react"; +import { + MouseEventHandler, + ReactNode, + useCallback, + useEffect, + useState, +} from "react"; import classNames from "classnames"; import "@formatjs/intl-durationformat/polyfill"; import { DurationFormat } from "@formatjs/intl-durationformat"; @@ -23,13 +29,26 @@ export function RaisedHandIndicator({ raisedHandTime, minature, showTimer, + onClick, }: { raisedHandTime?: Date; minature?: boolean; showTimer?: boolean; + onClick?: () => void; }): ReactNode { const [raisedHandDuration, setRaisedHandDuration] = useState(""); + const clickCallback = useCallback>( + (event) => { + if (!onClick) { + return; + } + event.preventDefault(); + onClick(); + }, + [onClick], + ); + // This effect creates a simple timer effect. useEffect(() => { if (!raisedHandTime || !showTimer) { @@ -52,26 +71,40 @@ export function RaisedHandIndicator({ return (): void => clearInterval(to); }, [setRaisedHandDuration, raisedHandTime, showTimer]); - if (raisedHandTime) { - return ( + if (!raisedHandTime) { + return; + } + + const content = ( +
-
- - ✋ - -
- {showTimer &&

{raisedHandDuration}

} + + ✋ +
+ {showTimer &&

{raisedHandDuration}

} +
+ ); + + if (onClick) { + return ( + ); } - return null; + return content; } diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx index 8252d1086..3ea40c5b3 100644 --- a/src/tile/GridTile.tsx +++ b/src/tile/GridTile.tsx @@ -92,7 +92,7 @@ const UserMediaTile = forwardRef( }, [vm], ); - const { raisedHands } = useReactions(); + const { raisedHands, lowerHand } = useReactions(); const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon; @@ -111,6 +111,8 @@ const UserMediaTile = forwardRef( ); const handRaised: Date | undefined = raisedHands[vm.member?.userId ?? ""]; + const raisedHandOnClick = + vm.local && handRaised ? (): void => void lowerHand() : undefined; const showSpeaking = showSpeakingIndicators && speaking; @@ -153,6 +155,7 @@ const UserMediaTile = forwardRef( } raisedHandTime={handRaised} + raisedHandOnClick={raisedHandOnClick} {...props} /> ); diff --git a/src/tile/MediaView.tsx b/src/tile/MediaView.tsx index d8b03dc98..b14f0ac3d 100644 --- a/src/tile/MediaView.tsx +++ b/src/tile/MediaView.tsx @@ -35,6 +35,7 @@ interface Props extends ComponentProps { displayName: string; primaryButton?: ReactNode; raisedHandTime?: Date; + raisedHandOnClick?: () => void; } export const MediaView = forwardRef( @@ -54,6 +55,7 @@ export const MediaView = forwardRef( displayName, primaryButton, raisedHandTime, + raisedHandOnClick, ...props }, ref, @@ -97,6 +99,7 @@ export const MediaView = forwardRef( raisedHandTime={raisedHandTime} minature={avatarSize < 96} showTimer={handRaiseTimerVisible} + onClick={raisedHandOnClick} />
{nameTagLeadingIcon} diff --git a/src/useReactions.test.tsx b/src/useReactions.test.tsx index 79caeb0af..67147d52e 100644 --- a/src/useReactions.test.tsx +++ b/src/useReactions.test.tsx @@ -45,7 +45,7 @@ const membership: Record = { }; const TestComponent: FC = () => { - const { raisedHands, myReactionId } = useReactions(); + const { raisedHands } = useReactions(); return (
    @@ -56,7 +56,6 @@ const TestComponent: FC = () => { ))}
-

{myReactionId ? "Local reaction" : "No local reaction"}

); }; @@ -172,15 +171,6 @@ describe("useReactions", () => { ); expect(queryByRole("list")?.children).to.have.lengthOf(0); }); - test("handles own raised hand", async () => { - const room = new MockRoom(); - const rtcSession = new MockRTCSession(room); - const { queryByText } = render( - , - ); - await act(() => room.testSendReaction(memberEventAlice)); - expect(queryByText("Local reaction")).toBeTruthy(); - }); test("handles incoming raised hand", async () => { const room = new MockRoom(); const rtcSession = new MockRTCSession(room); diff --git a/src/useReactions.tsx b/src/useReactions.tsx index 330318478..7ce478b5a 100644 --- a/src/useReactions.tsx +++ b/src/useReactions.tsx @@ -30,7 +30,7 @@ import { useClientState } from "./ClientContext"; interface ReactionsContextType { raisedHands: Record; supportsReactions: boolean; - myReactionId: string | null; + lowerHand: () => Promise; } const ReactionsContext = createContext( @@ -80,13 +80,6 @@ export const ReactionsProvider = ({ const room = rtcSession.room; const myUserId = room.client.getUserId(); - // Calculate our own reaction event. - const myReactionId = useMemo( - (): string | null => - (myUserId && raisedHands[myUserId]?.reactionEventId) ?? null, - [raisedHands, myUserId], - ); - // Reduce the data down for the consumers. const resultRaisedHands = useMemo( () => @@ -235,12 +228,37 @@ export const ReactionsProvider = ({ }; }, [room, addRaisedHand, removeRaisedHand, memberships, raisedHands]); + const lowerHand = useCallback(async () => { + if ( + !myUserId || + clientState?.state !== "valid" || + !clientState.authenticated || + !raisedHands[myUserId] + ) { + return; + } + const myReactionId = raisedHands[myUserId].reactionEventId; + if (!myReactionId) { + logger.warn(`Hand raised but no reaction event to redact!`); + return; + } + try { + await clientState.authenticated.client.redactEvent( + rtcSession.room.roomId, + myReactionId, + ); + logger.debug("Redacted raise hand event"); + } catch (ex) { + logger.error("Failed to redact reaction event", myReactionId, ex); + } + }, [myUserId, raisedHands, clientState, rtcSession]); + return ( {children}