/* Copyright 2024 Milton Moura SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import { EventType, RelationType } from "matrix-js-sdk"; import { createContext, useContext, type ReactNode, useCallback, useMemo, type JSX, } from "react"; import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc"; import { logger } from "matrix-js-sdk/lib/logger"; import { useObservableEagerState } from "observable-hooks"; import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships"; import { useClientState } from "../ClientContext"; import { ElementCallReactionEventType, type ReactionOption } from "."; import { type CallViewModel } from "../state/CallViewModel"; interface ReactionsSenderContextType { supportsReactions: boolean; toggleRaisedHand: () => Promise; sendReaction: (reaction: ReactionOption) => Promise; } const ReactionsSenderContext = createContext< ReactionsSenderContextType | undefined >(undefined); export const useReactionsSender = (): ReactionsSenderContextType => { const context = useContext(ReactionsSenderContext); if (!context) { throw new Error("useReactions must be used within a ReactionsProvider"); } return context; }; /** * Provider that handles sending a reaction or hand raised event to a call. */ export const ReactionsSenderProvider = ({ children, rtcSession, vm, }: { children: ReactNode; rtcSession: MatrixRTCSession; vm: CallViewModel; }): JSX.Element => { const memberships = useMatrixRTCSessionMemberships(rtcSession); const clientState = useClientState(); const supportsReactions = clientState?.state === "valid" && clientState.supportedFeatures.reactions; const room = rtcSession.room; const myUserId = room.client.getUserId(); const myDeviceId = room.client.getDeviceId(); const myMembershipIdentifier = `${myUserId}:${myDeviceId}`; const myMembershipEvent = useMemo( () => memberships.find( (m) => m.sender === myUserId && m.deviceId === myDeviceId, )?.eventId, [memberships, myUserId, myDeviceId], ); const reactions = useObservableEagerState(vm.reactions$); const myReaction = useMemo( () => myMembershipIdentifier !== undefined ? reactions[myMembershipIdentifier] : undefined, [myMembershipIdentifier, reactions], ); const handsRaised = useObservableEagerState(vm.handsRaised$); const myRaisedHand = useMemo( () => myMembershipIdentifier !== undefined ? handsRaised[myMembershipIdentifier] : undefined, [myMembershipIdentifier, handsRaised], ); const toggleRaisedHand = useCallback(async () => { if (!myMembershipIdentifier) { return; } const myReactionId = myRaisedHand?.reactionEventId; if (!myReactionId) { try { if (!myMembershipEvent) { 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: myMembershipEvent, 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; } } }, [ myMembershipEvent, myMembershipIdentifier, myRaisedHand, rtcSession, room, ]); const sendReaction = useCallback( async (reaction: ReactionOption) => { if (!myMembershipIdentifier || myReaction) { // We're still reacting return; } if (!myMembershipEvent) { 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: myMembershipEvent, }, emoji: reaction.emoji, name: reaction.name, }, ); }, [myMembershipEvent, myReaction, room, myMembershipIdentifier, rtcSession], ); return ( {children} ); };