Add keyboard shortcuts for raised hand / reactions (#2784)

* Add support for reactions / raised-hands via keyboard shortcuts.

* Add tests

* Fixup shortcuts

* update snapshotr

* fix type

* keyshortcuts

* remove mistakenly commited file

* fix label logic

* Add renderer for call joined / left

* Use caption

* lint

* remove unexpected file

* remove other unexpected change

* Remove other other unexpected change.
This commit is contained in:
Will Hunt
2024-11-19 16:57:57 +00:00
committed by GitHub
parent ffbc48fe86
commit b3ceb5300c
9 changed files with 173 additions and 127 deletions

View File

@@ -7,7 +7,6 @@ Please see LICENSE in the repository root for full details.
import { render } from "@testing-library/react"; import { render } from "@testing-library/react";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
import { TooltipProvider } from "@vector-im/compound-web"; import { TooltipProvider } from "@vector-im/compound-web";
import { userEvent } from "@testing-library/user-event"; import { userEvent } from "@testing-library/user-event";
import { ReactNode } from "react"; import { ReactNode } from "react";
@@ -29,18 +28,13 @@ const membership: Record<string, string> = {
function TestComponent({ function TestComponent({
rtcSession, rtcSession,
room,
}: { }: {
rtcSession: MockRTCSession; rtcSession: MockRTCSession;
room: MockRoom;
}): ReactNode { }): ReactNode {
return ( return (
<TooltipProvider> <TooltipProvider>
<TestReactionsWrapper rtcSession={rtcSession}> <TestReactionsWrapper rtcSession={rtcSession}>
<ReactionToggleButton <ReactionToggleButton userId={memberUserIdAlice} />
rtcSession={rtcSession as unknown as MatrixRTCSession}
client={room.client}
/>
</TestReactionsWrapper> </TestReactionsWrapper>
</TooltipProvider> </TooltipProvider>
); );
@@ -51,7 +45,7 @@ test("Can open menu", async () => {
const room = new MockRoom(memberUserIdAlice); const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership); const rtcSession = new MockRTCSession(room, membership);
const { getByLabelText, container } = render( const { getByLabelText, container } = render(
<TestComponent rtcSession={rtcSession} room={room} />, <TestComponent rtcSession={rtcSession} />,
); );
await user.click(getByLabelText("common.reactions")); await user.click(getByLabelText("common.reactions"));
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
@@ -62,7 +56,7 @@ test("Can raise hand", async () => {
const room = new MockRoom(memberUserIdAlice); const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership); const rtcSession = new MockRTCSession(room, membership);
const { getByLabelText, container } = render( const { getByLabelText, container } = render(
<TestComponent rtcSession={rtcSession} room={room} />, <TestComponent rtcSession={rtcSession} />,
); );
await user.click(getByLabelText("common.reactions")); await user.click(getByLabelText("common.reactions"));
await user.click(getByLabelText("action.raise_hand")); await user.click(getByLabelText("action.raise_hand"));
@@ -87,7 +81,7 @@ test("Can lower hand", async () => {
const room = new MockRoom(memberUserIdAlice); const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership); const rtcSession = new MockRTCSession(room, membership);
const { getByLabelText, container } = render( const { getByLabelText, container } = render(
<TestComponent rtcSession={rtcSession} room={room} />, <TestComponent rtcSession={rtcSession} />,
); );
const reactionEvent = room.testSendHandRaise(memberEventAlice, membership); const reactionEvent = room.testSendHandRaise(memberEventAlice, membership);
await user.click(getByLabelText("common.reactions")); await user.click(getByLabelText("common.reactions"));
@@ -101,7 +95,7 @@ test("Can react with emoji", async () => {
const room = new MockRoom(memberUserIdAlice); const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership); const rtcSession = new MockRTCSession(room, membership);
const { getByLabelText, getByText } = render( const { getByLabelText, getByText } = render(
<TestComponent rtcSession={rtcSession} room={room} />, <TestComponent rtcSession={rtcSession} />,
); );
await user.click(getByLabelText("common.reactions")); await user.click(getByLabelText("common.reactions"));
await user.click(getByText("🐶")); await user.click(getByText("🐶"));
@@ -126,7 +120,7 @@ test("Can fully expand emoji picker", async () => {
const room = new MockRoom(memberUserIdAlice); const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership); const rtcSession = new MockRTCSession(room, membership);
const { getByText, container, getByLabelText } = render( const { getByText, container, getByLabelText } = render(
<TestComponent rtcSession={rtcSession} room={room} />, <TestComponent rtcSession={rtcSession} />,
); );
await user.click(getByLabelText("common.reactions")); await user.click(getByLabelText("common.reactions"));
await user.click(getByLabelText("action.show_more")); await user.click(getByLabelText("action.show_more"));
@@ -149,12 +143,12 @@ test("Can fully expand emoji picker", async () => {
]); ]);
}); });
test("Can close search", async () => { test("Can close reaction dialog", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice); const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership); const rtcSession = new MockRTCSession(room, membership);
const { getByLabelText, container } = render( const { getByLabelText, container } = render(
<TestComponent rtcSession={rtcSession} room={room} />, <TestComponent rtcSession={rtcSession} />,
); );
await user.click(getByLabelText("common.reactions")); await user.click(getByLabelText("common.reactions"));
await user.click(getByLabelText("action.show_more")); await user.click(getByLabelText("action.show_more"));

View File

@@ -23,19 +23,11 @@ import {
} from "react"; } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { EventType, RelationType } from "matrix-js-sdk/src/matrix";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import classNames from "classnames"; import classNames from "classnames";
import { useReactions } from "../useReactions"; import { useReactions } from "../useReactions";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
import styles from "./ReactionToggleButton.module.css"; import styles from "./ReactionToggleButton.module.css";
import { import { ReactionOption, ReactionSet, ReactionsRowSize } from "../reactions";
ReactionOption,
ReactionSet,
ElementCallReactionEventType,
} from "../reactions";
import { Modal } from "../Modal"; import { Modal } from "../Modal";
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> { interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
@@ -95,9 +87,10 @@ export function ReactionPopupMenu({
)} )}
<div className={styles.reactionPopupMenu}> <div className={styles.reactionPopupMenu}>
<section className={styles.handRaiseSection}> <section className={styles.handRaiseSection}>
<Tooltip label={label}> <Tooltip label={label} caption="H">
<CpdButton <CpdButton
kind={isHandRaised ? "primary" : "secondary"} kind={isHandRaised ? "primary" : "secondary"}
aria-keyshortcuts="H"
aria-pressed={isHandRaised} aria-pressed={isHandRaised}
aria-label={label} aria-label={label}
onClick={() => toggleRaisedHand()} onClick={() => toggleRaisedHand()}
@@ -114,14 +107,26 @@ export function ReactionPopupMenu({
styles.reactionsMenu, styles.reactionsMenu,
)} )}
> >
{filteredReactionSet.map((reaction) => ( {filteredReactionSet.map((reaction, index) => (
<li key={reaction.name}> <li key={reaction.name}>
<Tooltip label={reaction.name}> <Tooltip
label={reaction.name}
caption={
index < ReactionsRowSize
? (index + 1).toString()
: undefined
}
>
<CpdButton <CpdButton
kind="secondary" kind="secondary"
className={styles.reactionButton} className={styles.reactionButton}
disabled={!canReact} disabled={!canReact}
onClick={() => sendReaction(reaction)} onClick={() => sendReaction(reaction)}
aria-keyshortcuts={
index < ReactionsRowSize
? (index + 1).toString()
: undefined
}
> >
{reaction.emoji} {reaction.emoji}
</CpdButton> </CpdButton>
@@ -153,52 +158,33 @@ export function ReactionPopupMenu({
} }
interface ReactionToggleButtonProps extends ComponentPropsWithoutRef<"button"> { interface ReactionToggleButtonProps extends ComponentPropsWithoutRef<"button"> {
rtcSession: MatrixRTCSession; userId: string;
client: MatrixClient;
} }
export function ReactionToggleButton({ export function ReactionToggleButton({
client, userId,
rtcSession,
...props ...props
}: ReactionToggleButtonProps): ReactNode { }: ReactionToggleButtonProps): ReactNode {
const { t } = useTranslation(); const { t } = useTranslation();
const { raisedHands, lowerHand, reactions } = useReactions(); const { raisedHands, toggleRaisedHand, sendReaction, reactions } =
useReactions();
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const userId = client.getUserId()!;
const isHandRaised = !!raisedHands[userId];
const memberships = useMatrixRTCSessionMemberships(rtcSession);
const [showReactionsMenu, setShowReactionsMenu] = useState(false); const [showReactionsMenu, setShowReactionsMenu] = useState(false);
const [errorText, setErrorText] = useState<string>(); const [errorText, setErrorText] = useState<string>();
const isHandRaised = !!raisedHands[userId];
const canReact = !reactions[userId];
useEffect(() => { useEffect(() => {
// Clear whenever the reactions menu state changes. // Clear whenever the reactions menu state changes.
setErrorText(undefined); setErrorText(undefined);
}, [showReactionsMenu]); }, [showReactionsMenu]);
const canReact = !reactions[userId];
const sendRelation = useCallback( const sendRelation = useCallback(
async (reaction: ReactionOption) => { async (reaction: ReactionOption) => {
try { try {
const myMembership = memberships.find((m) => m.sender === userId);
if (!myMembership?.eventId) {
throw new Error("Cannot find own membership event");
}
const parentEventId = myMembership.eventId;
setBusy(true); setBusy(true);
await client.sendEvent( await sendReaction(reaction);
rtcSession.room.roomId,
ElementCallReactionEventType,
{
"m.relates_to": {
rel_type: RelationType.Reference,
event_id: parentEventId,
},
emoji: reaction.emoji,
name: reaction.name,
},
);
setErrorText(undefined); setErrorText(undefined);
setShowReactionsMenu(false); setShowReactionsMenu(false);
} catch (ex) { } catch (ex) {
@@ -208,59 +194,25 @@ export function ReactionToggleButton({
setBusy(false); setBusy(false);
} }
}, },
[memberships, client, userId, rtcSession], [sendReaction],
); );
const toggleRaisedHand = useCallback(() => { const wrappedToggleRaisedHand = useCallback(() => {
const raiseHand = async (): Promise<void> => { const toggleHand = async (): Promise<void> => {
if (isHandRaised) { try {
try { setBusy(true);
setBusy(true); await toggleRaisedHand();
await lowerHand(); setShowReactionsMenu(false);
setShowReactionsMenu(false); } catch (ex) {
} finally { setErrorText(ex instanceof Error ? ex.message : "Unknown error");
setBusy(false); logger.error("Failed to raise/lower hand", ex);
} } finally {
} else { setBusy(false);
try {
const myMembership = memberships.find((m) => m.sender === userId);
if (!myMembership?.eventId) {
throw new Error("Cannot find own membership event");
}
const parentEventId = myMembership.eventId;
setBusy(true);
const reaction = await client.sendEvent(
rtcSession.room.roomId,
EventType.Reaction,
{
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: parentEventId,
key: "🖐️",
},
},
);
logger.debug("Sent raise hand event", reaction.event_id);
setErrorText(undefined);
setShowReactionsMenu(false);
} catch (ex) {
setErrorText(ex instanceof Error ? ex.message : "Unknown error");
logger.error("Failed to raise hand", ex);
} finally {
setBusy(false);
}
} }
}; };
void raiseHand(); void toggleHand();
}, [ }, [toggleRaisedHand]);
client,
isHandRaised,
memberships,
lowerHand,
rtcSession.room.roomId,
userId,
]);
return ( return (
<> <>
@@ -284,7 +236,7 @@ export function ReactionToggleButton({
isHandRaised={isHandRaised} isHandRaised={isHandRaised}
canReact={!busy && canReact} canReact={!busy && canReact}
sendReaction={(reaction) => void sendRelation(reaction)} sendReaction={(reaction) => void sendRelation(reaction)}
toggleRaisedHand={toggleRaisedHand} toggleRaisedHand={wrappedToggleRaisedHand}
/> />
</Modal> </Modal>
</> </>

View File

@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Can close search 1`] = ` exports[`Can close reaction dialog 1`] = `
<div <div
aria-hidden="true" aria-hidden="true"
data-aria-hidden="true" data-aria-hidden="true"

View File

@@ -73,7 +73,9 @@ export const GenericReaction: ReactionOption = {
}, },
}; };
// The first 6 reactions are always visible. export const ReactionsRowSize = 5;
// The first {ReactionsRowSize} reactions are always visible.
export const ReactionSet: ReactionOption[] = [ export const ReactionSet: ReactionOption[] = [
{ {
emoji: "👍", emoji: "👍",

View File

@@ -183,7 +183,8 @@ export const InCallView: FC<InCallViewProps> = ({
onShareClick, onShareClick,
}) => { }) => {
const [soundEffectVolume] = useSetting(soundEffectVolumeSetting); const [soundEffectVolume] = useSetting(soundEffectVolumeSetting);
const { supportsReactions, raisedHands } = useReactions(); const { supportsReactions, raisedHands, sendReaction, toggleRaisedHand } =
useReactions();
const raisedHandCount = useMemo( const raisedHandCount = useMemo(
() => Object.keys(raisedHands).length, () => Object.keys(raisedHands).length,
[raisedHands], [raisedHands],
@@ -227,6 +228,8 @@ export const InCallView: FC<InCallViewProps> = ({
toggleMicrophone, toggleMicrophone,
toggleCamera, toggleCamera,
(muted) => muteStates.audio.setEnabled?.(!muted), (muted) => muteStates.audio.setEnabled?.(!muted),
(reaction) => void sendReaction(reaction),
() => void toggleRaisedHand(),
); );
const windowMode = useObservableEagerState(vm.windowMode); const windowMode = useObservableEagerState(vm.windowMode);
@@ -572,8 +575,7 @@ export const InCallView: FC<InCallViewProps> = ({
<ReactionToggleButton <ReactionToggleButton
key="raise_hand" key="raise_hand"
className={styles.raiseHand} className={styles.raiseHand}
client={client} userId={client.getUserId()!}
rtcSession={rtcSession}
onTouchEnd={onControlsTouchEnd} onTouchEnd={onControlsTouchEnd}
/>, />,
); );

View File

@@ -97,7 +97,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
}, },
[vm], [vm],
); );
const { raisedHands, lowerHand, reactions } = useReactions(); const { raisedHands, toggleRaisedHand, reactions } = useReactions();
const AudioIcon = locallyMuted const AudioIcon = locallyMuted
? VolumeOffSolidIcon ? VolumeOffSolidIcon
@@ -127,8 +127,9 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
const handRaised: Date | undefined = raisedHands[vm.member?.userId ?? ""]; const handRaised: Date | undefined = raisedHands[vm.member?.userId ?? ""];
const currentReaction: ReactionOption | undefined = const currentReaction: ReactionOption | undefined =
reactions[vm.member?.userId ?? ""]; reactions[vm.member?.userId ?? ""];
const raisedHandOnClick = const raisedHandOnClick = vm.local
vm.local && handRaised ? (): void => void lowerHand() : undefined; ? (): void => void toggleRaisedHand()
: undefined;
const showSpeaking = showSpeakingIndicators && speaking; const showSpeaking = showSpeakingIndicators && speaking;

View File

@@ -12,19 +12,24 @@ import { Button } from "@vector-im/compound-web";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { useCallViewKeyboardShortcuts } from "../src/useCallViewKeyboardShortcuts"; import { useCallViewKeyboardShortcuts } from "../src/useCallViewKeyboardShortcuts";
import { ReactionOption, ReactionSet, ReactionsRowSize } from "./reactions";
// Test Explanation: // Test Explanation:
// - The main objective is to test `useCallViewKeyboardShortcuts`. // - The main objective is to test `useCallViewKeyboardShortcuts`.
// The TestComponent just wraps a button around that hook. // The TestComponent just wraps a button around that hook.
interface TestComponentProps { interface TestComponentProps {
setMicrophoneMuted: (muted: boolean) => void; setMicrophoneMuted?: (muted: boolean) => void;
onButtonClick?: () => void; onButtonClick?: () => void;
sendReaction?: () => void;
toggleHandRaised?: () => void;
} }
const TestComponent: FC<TestComponentProps> = ({ const TestComponent: FC<TestComponentProps> = ({
setMicrophoneMuted, setMicrophoneMuted = (): void => {},
onButtonClick = (): void => {}, onButtonClick = (): void => {},
sendReaction = (reaction: ReactionOption): void => {},
toggleHandRaised = (): void => {},
}) => { }) => {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
useCallViewKeyboardShortcuts( useCallViewKeyboardShortcuts(
@@ -32,6 +37,8 @@ const TestComponent: FC<TestComponentProps> = ({
() => {}, () => {},
() => {}, () => {},
setMicrophoneMuted, setMicrophoneMuted,
sendReaction,
toggleHandRaised,
); );
return ( return (
<div ref={ref}> <div ref={ref}>
@@ -74,6 +81,28 @@ test("spacebar prioritizes pressing a button", async () => {
expect(onClick).toBeCalled(); expect(onClick).toBeCalled();
}); });
test("reactions can be sent via keyboard presses", async () => {
const user = userEvent.setup();
const sendReaction = vi.fn();
render(<TestComponent sendReaction={sendReaction} />);
for (let index = 1; index <= ReactionsRowSize; index++) {
await user.keyboard(index.toString());
expect(sendReaction).toHaveBeenNthCalledWith(index, ReactionSet[index - 1]);
}
});
test("raised hand can be sent via keyboard presses", async () => {
const user = userEvent.setup();
const toggleHandRaised = vi.fn();
render(<TestComponent toggleHandRaised={toggleHandRaised} />);
await user.keyboard("h");
expect(toggleHandRaised).toHaveBeenCalledOnce();
});
test("unmuting happens in place of the default action", async () => { test("unmuting happens in place of the default action", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const defaultPrevented = vi.fn(); const defaultPrevented = vi.fn();

View File

@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
import { RefObject, useCallback, useMemo, useRef } from "react"; import { RefObject, useCallback, useMemo, useRef } from "react";
import { useEventTarget } from "./useEvents"; import { useEventTarget } from "./useEvents";
import { ReactionOption, ReactionSet, ReactionsRowSize } from "./reactions";
/** /**
* Determines whether focus is in the same part of the tree as the given * Determines whether focus is in the same part of the tree as the given
@@ -18,11 +19,17 @@ const mayReceiveKeyEvents = (e: HTMLElement): boolean => {
return focusedElement !== null && focusedElement.contains(e); return focusedElement !== null && focusedElement.contains(e);
}; };
const KeyToReactionMap: Record<string, ReactionOption> = Object.fromEntries(
ReactionSet.slice(0, ReactionsRowSize).map((r, i) => [(i + 1).toString(), r]),
);
export function useCallViewKeyboardShortcuts( export function useCallViewKeyboardShortcuts(
focusElement: RefObject<HTMLElement | null>, focusElement: RefObject<HTMLElement | null>,
toggleMicrophoneMuted: () => void, toggleMicrophoneMuted: () => void,
toggleLocalVideoMuted: () => void, toggleLocalVideoMuted: () => void,
setMicrophoneMuted: (muted: boolean) => void, setMicrophoneMuted: (muted: boolean) => void,
sendReaction: (reaction: ReactionOption) => void,
toggleHandRaised: () => void,
): void { ): void {
const spacebarHeld = useRef(false); const spacebarHeld = useRef(false);
@@ -49,6 +56,12 @@ export function useCallViewKeyboardShortcuts(
spacebarHeld.current = true; spacebarHeld.current = true;
setMicrophoneMuted(false); setMicrophoneMuted(false);
} }
} else if (event.key === "h") {
event.preventDefault();
toggleHandRaised();
} else if (KeyToReactionMap[event.key]) {
event.preventDefault();
sendReaction(KeyToReactionMap[event.key]);
} }
}, },
[ [
@@ -56,6 +69,8 @@ export function useCallViewKeyboardShortcuts(
toggleLocalVideoMuted, toggleLocalVideoMuted,
toggleMicrophoneMuted, toggleMicrophoneMuted,
setMicrophoneMuted, setMicrophoneMuted,
sendReaction,
toggleHandRaised,
], ],
), ),
// Because this is set on the window, to prevent shortcuts from activating // Because this is set on the window, to prevent shortcuts from activating

View File

@@ -40,7 +40,8 @@ interface ReactionsContextType {
raisedHands: Record<string, Date>; raisedHands: Record<string, Date>;
supportsReactions: boolean; supportsReactions: boolean;
reactions: Record<string, ReactionOption>; reactions: Record<string, ReactionOption>;
lowerHand: () => Promise<void>; toggleRaisedHand: () => Promise<void>;
sendReaction: (reaction: ReactionOption) => Promise<void>;
} }
const ReactionsContext = createContext<ReactionsContextType | undefined>( const ReactionsContext = createContext<ReactionsContextType | undefined>(
@@ -104,7 +105,6 @@ export const ReactionsProvider = ({
), ),
[raisedHands], [raisedHands],
); );
const addRaisedHand = useCallback((userId: string, info: RaisedHandInfo) => { const addRaisedHand = useCallback((userId: string, info: RaisedHandInfo) => {
setRaisedHands((prevRaisedHands) => ({ setRaisedHands((prevRaisedHands) => ({
...prevRaisedHands, ...prevRaisedHands,
@@ -181,6 +181,11 @@ export const ReactionsProvider = ({
const latestMemberships = useLatest(memberships); const latestMemberships = useLatest(memberships);
const latestRaisedHands = useLatest(raisedHands); const latestRaisedHands = useLatest(raisedHands);
const myMembership = useMemo(
() => memberships.find((m) => m.sender === myUserId)?.eventId,
[memberships, myUserId],
);
// This effect handles any *live* reaction/redactions in the room. // This effect handles any *live* reaction/redactions in the room.
useEffect(() => { useEffect(() => {
const reactionTimeouts = new Set<number>(); const reactionTimeouts = new Set<number>();
@@ -322,22 +327,67 @@ export const ReactionsProvider = ({
latestRaisedHands, latestRaisedHands,
]); ]);
const lowerHand = useCallback(async () => { const toggleRaisedHand = useCallback(async () => {
if (!myUserId || !raisedHands[myUserId]) { if (!myUserId) {
return; return;
} }
const myReactionId = raisedHands[myUserId].reactionEventId; const myReactionId = raisedHands[myUserId]?.reactionEventId;
if (!myReactionId) { if (!myReactionId) {
logger.warn(`Hand raised but no reaction event to redact!`); try {
return; if (!myMembership) {
throw new Error("Cannot find own membership event");
}
const reaction = await room.client.sendEvent(
rtcSession.room.roomId,
EventType.Reaction,
{
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: myMembership,
key: "🖐️",
},
},
);
logger.debug("Sent raise hand event", reaction.event_id);
} catch (ex) {
logger.error("Failed to send raised hand", ex);
}
} else {
try {
await room.client.redactEvent(rtcSession.room.roomId, myReactionId);
logger.debug("Redacted raise hand event");
} catch (ex) {
logger.error("Failed to redact reaction event", myReactionId, ex);
throw ex;
}
} }
try { }, [myMembership, myUserId, raisedHands, rtcSession, room]);
await room.client.redactEvent(rtcSession.room.roomId, myReactionId);
logger.debug("Redacted raise hand event"); const sendReaction = useCallback(
} catch (ex) { async (reaction: ReactionOption) => {
logger.error("Failed to redact reaction event", myReactionId, ex); if (!myUserId || reactions[myUserId]) {
} // We're still reacting
}, [myUserId, raisedHands, rtcSession, room]); return;
}
if (!myMembership) {
throw new Error("Cannot find own membership event");
}
await room.client.sendEvent(
rtcSession.room.roomId,
ElementCallReactionEventType,
{
"m.relates_to": {
rel_type: RelationType.Reference,
event_id: myMembership,
},
emoji: reaction.emoji,
name: reaction.name,
},
);
},
[myMembership, reactions, room, myUserId, rtcSession],
);
return ( return (
<ReactionsContext.Provider <ReactionsContext.Provider
@@ -345,7 +395,8 @@ export const ReactionsProvider = ({
raisedHands: resultRaisedHands, raisedHands: resultRaisedHands,
supportsReactions, supportsReactions,
reactions, reactions,
lowerHand, toggleRaisedHand,
sendReaction,
}} }}
> >
{children} {children}