Skip to content

Commit bc0ab92

Browse files
authored
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
1 parent 110914a commit bc0ab92

File tree

8 files changed

+106
-49
lines changed

8 files changed

+106
-49
lines changed

src/button/RaisedHandToggleButton.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function RaiseHandToggleButton({
6262
client,
6363
rtcSession,
6464
}: RaisedHandToggleButtonProps): ReactNode {
65-
const { raisedHands, myReactionId } = useReactions();
65+
const { raisedHands, lowerHand } = useReactions();
6666
const [busy, setBusy] = useState(false);
6767
const userId = client.getUserId()!;
6868
const isHandRaised = !!raisedHands[userId];
@@ -71,16 +71,9 @@ export function RaiseHandToggleButton({
7171
const toggleRaisedHand = useCallback(() => {
7272
const raiseHand = async (): Promise<void> => {
7373
if (isHandRaised) {
74-
if (!myReactionId) {
75-
logger.warn(`Hand raised but no reaction event to redact!`);
76-
return;
77-
}
7874
try {
7975
setBusy(true);
80-
await client.redactEvent(rtcSession.room.roomId, myReactionId);
81-
logger.debug("Redacted raise hand event");
82-
} catch (ex) {
83-
logger.error("Failed to redact reaction event", myReactionId, ex);
76+
await lowerHand();
8477
} finally {
8578
setBusy(false);
8679
}
@@ -118,9 +111,9 @@ export function RaiseHandToggleButton({
118111
client,
119112
isHandRaised,
120113
memberships,
121-
myReactionId,
122114
rtcSession.room.roomId,
123115
userId,
116+
lowerHand,
124117
]);
125118

126119
return (

src/reactions/RaisedHandIndicator.module.css

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
color: var(--cpd-color-icon-secondary);
66
}
77

8+
.button {
9+
display: contents;
10+
background: none;
11+
}
12+
813
.raisedHandWidget > p {
914
padding: none;
1015
margin-top: auto;
@@ -42,11 +47,11 @@
4247
height: var(--cpd-space-6x);
4348
display: inline-block;
4449
text-align: center;
45-
font-size: 16px;
50+
font-size: 1.3em;
4651
}
4752

4853
.raisedHandLarge > span {
4954
width: var(--cpd-space-8x);
5055
height: var(--cpd-space-8x);
51-
font-size: 22px;
56+
font-size: 1.9em;
5257
}

src/reactions/RaisedHandIndicator.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,16 @@ describe("RaisedHandIndicator", () => {
4040
);
4141
expect(container.firstChild).toMatchSnapshot();
4242
});
43+
test("can be clicked", () => {
44+
const dateTime = new Date();
45+
let wasClicked = false;
46+
const { getByRole } = render(
47+
<RaisedHandIndicator
48+
raisedHandTime={dateTime}
49+
onClick={() => (wasClicked = true)}
50+
/>,
51+
);
52+
getByRole("button").click();
53+
expect(wasClicked).toBe(true);
54+
});
4355
});

src/reactions/RaisedHandIndicator.tsx

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
55
Please see LICENSE in the repository root for full details.
66
*/
77

8-
import { ReactNode, useEffect, useState } from "react";
8+
import {
9+
MouseEventHandler,
10+
ReactNode,
11+
useCallback,
12+
useEffect,
13+
useState,
14+
} from "react";
915
import classNames from "classnames";
1016
import "@formatjs/intl-durationformat/polyfill";
1117
import { DurationFormat } from "@formatjs/intl-durationformat";
@@ -23,13 +29,26 @@ export function RaisedHandIndicator({
2329
raisedHandTime,
2430
minature,
2531
showTimer,
32+
onClick,
2633
}: {
2734
raisedHandTime?: Date;
2835
minature?: boolean;
2936
showTimer?: boolean;
37+
onClick?: () => void;
3038
}): ReactNode {
3139
const [raisedHandDuration, setRaisedHandDuration] = useState("");
3240

41+
const clickCallback = useCallback<MouseEventHandler<HTMLButtonElement>>(
42+
(event) => {
43+
if (!onClick) {
44+
return;
45+
}
46+
event.preventDefault();
47+
onClick();
48+
},
49+
[onClick],
50+
);
51+
3352
// This effect creates a simple timer effect.
3453
useEffect(() => {
3554
if (!raisedHandTime || !showTimer) {
@@ -52,26 +71,40 @@ export function RaisedHandIndicator({
5271
return (): void => clearInterval(to);
5372
}, [setRaisedHandDuration, raisedHandTime, showTimer]);
5473

55-
if (raisedHandTime) {
56-
return (
74+
if (!raisedHandTime) {
75+
return;
76+
}
77+
78+
const content = (
79+
<div
80+
className={classNames(styles.raisedHandWidget, {
81+
[styles.raisedHandWidgetLarge]: !minature,
82+
})}
83+
>
5784
<div
58-
className={classNames(styles.raisedHandWidget, {
59-
[styles.raisedHandWidgetLarge]: !minature,
85+
className={classNames(styles.raisedHand, {
86+
[styles.raisedHandLarge]: !minature,
6087
})}
6188
>
62-
<div
63-
className={classNames(styles.raisedHand, {
64-
[styles.raisedHandLarge]: !minature,
65-
})}
66-
>
67-
<span role="img" aria-label="raised hand">
68-
69-
</span>
70-
</div>
71-
{showTimer && <p>{raisedHandDuration}</p>}
89+
<span role="img" aria-label="raised hand">
90+
91+
</span>
7292
</div>
93+
{showTimer && <p>{raisedHandDuration}</p>}
94+
</div>
95+
);
96+
97+
if (onClick) {
98+
return (
99+
<button
100+
aria-label="lower raised hand"
101+
className={styles.button}
102+
onClick={clickCallback}
103+
>
104+
{content}
105+
</button>
73106
);
74107
}
75108

76-
return null;
109+
return content;
77110
}

src/tile/GridTile.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
9292
},
9393
[vm],
9494
);
95-
const { raisedHands } = useReactions();
95+
const { raisedHands, lowerHand } = useReactions();
9696

9797
const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon;
9898

@@ -111,6 +111,8 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
111111
);
112112

113113
const handRaised: Date | undefined = raisedHands[vm.member?.userId ?? ""];
114+
const raisedHandOnClick =
115+
vm.local && handRaised ? (): void => void lowerHand() : undefined;
114116

115117
const showSpeaking = showSpeakingIndicators && speaking;
116118

@@ -153,6 +155,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
153155
</Menu>
154156
}
155157
raisedHandTime={handRaised}
158+
raisedHandOnClick={raisedHandOnClick}
156159
{...props}
157160
/>
158161
);

src/tile/MediaView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ interface Props extends ComponentProps<typeof animated.div> {
3535
displayName: string;
3636
primaryButton?: ReactNode;
3737
raisedHandTime?: Date;
38+
raisedHandOnClick?: () => void;
3839
}
3940

4041
export const MediaView = forwardRef<HTMLDivElement, Props>(
@@ -54,6 +55,7 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
5455
displayName,
5556
primaryButton,
5657
raisedHandTime,
58+
raisedHandOnClick,
5759
...props
5860
},
5961
ref,
@@ -97,6 +99,7 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
9799
raisedHandTime={raisedHandTime}
98100
minature={avatarSize < 96}
99101
showTimer={handRaiseTimerVisible}
102+
onClick={raisedHandOnClick}
100103
/>
101104
<div className={styles.nameTag}>
102105
{nameTagLeadingIcon}

src/useReactions.test.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const membership: Record<string, string> = {
4545
};
4646

4747
const TestComponent: FC = () => {
48-
const { raisedHands, myReactionId } = useReactions();
48+
const { raisedHands } = useReactions();
4949
return (
5050
<div>
5151
<ul>
@@ -56,7 +56,6 @@ const TestComponent: FC = () => {
5656
</li>
5757
))}
5858
</ul>
59-
<p>{myReactionId ? "Local reaction" : "No local reaction"}</p>
6059
</div>
6160
);
6261
};
@@ -172,15 +171,6 @@ describe("useReactions", () => {
172171
);
173172
expect(queryByRole("list")?.children).to.have.lengthOf(0);
174173
});
175-
test("handles own raised hand", async () => {
176-
const room = new MockRoom();
177-
const rtcSession = new MockRTCSession(room);
178-
const { queryByText } = render(
179-
<TestComponentWrapper rtcSession={rtcSession} />,
180-
);
181-
await act(() => room.testSendReaction(memberEventAlice));
182-
expect(queryByText("Local reaction")).toBeTruthy();
183-
});
184174
test("handles incoming raised hand", async () => {
185175
const room = new MockRoom();
186176
const rtcSession = new MockRTCSession(room);

src/useReactions.tsx

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { useClientState } from "./ClientContext";
3030
interface ReactionsContextType {
3131
raisedHands: Record<string, Date>;
3232
supportsReactions: boolean;
33-
myReactionId: string | null;
33+
lowerHand: () => Promise<void>;
3434
}
3535

3636
const ReactionsContext = createContext<ReactionsContextType | undefined>(
@@ -80,13 +80,6 @@ export const ReactionsProvider = ({
8080
const room = rtcSession.room;
8181
const myUserId = room.client.getUserId();
8282

83-
// Calculate our own reaction event.
84-
const myReactionId = useMemo(
85-
(): string | null =>
86-
(myUserId && raisedHands[myUserId]?.reactionEventId) ?? null,
87-
[raisedHands, myUserId],
88-
);
89-
9083
// Reduce the data down for the consumers.
9184
const resultRaisedHands = useMemo(
9285
() =>
@@ -235,12 +228,37 @@ export const ReactionsProvider = ({
235228
};
236229
}, [room, addRaisedHand, removeRaisedHand, memberships, raisedHands]);
237230

231+
const lowerHand = useCallback(async () => {
232+
if (
233+
!myUserId ||
234+
clientState?.state !== "valid" ||
235+
!clientState.authenticated ||
236+
!raisedHands[myUserId]
237+
) {
238+
return;
239+
}
240+
const myReactionId = raisedHands[myUserId].reactionEventId;
241+
if (!myReactionId) {
242+
logger.warn(`Hand raised but no reaction event to redact!`);
243+
return;
244+
}
245+
try {
246+
await clientState.authenticated.client.redactEvent(
247+
rtcSession.room.roomId,
248+
myReactionId,
249+
);
250+
logger.debug("Redacted raise hand event");
251+
} catch (ex) {
252+
logger.error("Failed to redact reaction event", myReactionId, ex);
253+
}
254+
}, [myUserId, raisedHands, clientState, rtcSession]);
255+
238256
return (
239257
<ReactionsContext.Provider
240258
value={{
241259
raisedHands: resultRaisedHands,
242260
supportsReactions,
243-
myReactionId,
261+
lowerHand,
244262
}}
245263
>
246264
{children}

0 commit comments

Comments
 (0)