Files
element-call-Github/src/useReactions.tsx
2024-10-28 22:24:20 +00:00

165 lines
4.8 KiB
TypeScript

/*
Copyright 2024 Milton Moura <miltonmoura@gmail.com>
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
import {
EventType,
MatrixEvent,
RelationType,
RoomEvent as MatrixRoomEvent,
} from "matrix-js-sdk/src/matrix";
import { ReactionEventContent } from "matrix-js-sdk/src/types";
import {
createContext,
useContext,
useState,
ReactNode,
useCallback,
useEffect,
} from "react";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { useMatrixRTCSessionMemberships } from "./useMatrixRTCSessionMemberships";
import { useClientState } from "./ClientContext";
interface ReactionsContextType {
raisedHands: Record<string, Date>;
raisedHandCount: number;
addRaisedHand: (userId: string, date: Date) => void;
removeRaisedHand: (userId: string) => void;
supportsReactions: boolean;
myReactionId: string | null;
setMyReactionId: (id: string | null) => void;
}
const ReactionsContext = createContext<ReactionsContextType | undefined>(
undefined,
);
export const useReactions = (): ReactionsContextType => {
const context = useContext(ReactionsContext);
if (!context) {
throw new Error("useReactions must be used within a ReactionsProvider");
}
return context;
};
export const ReactionsProvider = ({
children,
rtcSession,
}: {
children: ReactNode;
rtcSession: MatrixRTCSession;
}): JSX.Element => {
const [raisedHands, setRaisedHands] = useState<Record<string, Date>>({});
const [myReactionId, setMyReactionId] = useState<string | null>(null);
const [raisedHandCount, setRaisedHandCount] = useState(0);
const memberships = useMatrixRTCSessionMemberships(rtcSession);
const clientState = useClientState();
const supportsReactions =
clientState?.state === "valid" && clientState.supportedFeatures.reactions;
const room = rtcSession.room;
const addRaisedHand = useCallback(
(userId: string, time: Date) => {
setRaisedHands({
...raisedHands,
[userId]: time,
});
setRaisedHandCount(Object.keys(raisedHands).length + 1);
},
[raisedHands],
);
const removeRaisedHand = useCallback(
(userId: string) => {
delete raisedHands[userId];
setRaisedHands(raisedHands);
setRaisedHandCount(Object.keys(raisedHands).length);
},
[raisedHands],
);
// Load any existing reactions.
useEffect(() => {
const getLastReactionEvent = (eventId: string): MatrixEvent | undefined => {
const relations = room.relations.getChildEventsForEvent(
eventId,
RelationType.Annotation,
EventType.Reaction,
);
const allEvents = relations?.getRelations() ?? [];
return allEvents.length > 0 ? allEvents[0] : undefined;
};
for (const m of memberships) {
if (!m.sender || !m.eventId) {
continue;
}
const reaction = getLastReactionEvent(m.eventId);
const eventId = reaction?.getId();
if (!eventId) {
continue;
}
if (reaction && reaction.getType() === EventType.Reaction) {
const content = reaction.getContent() as ReactionEventContent;
if (content?.["m.relates_to"]?.key === "🖐️") {
addRaisedHand(m.sender, new Date(reaction.localTimestamp));
if (m.sender === room.client.getUserId()) {
setMyReactionId(eventId);
}
}
}
}
// Deliberately ignoring addRaisedHand which was causing looping.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [room, memberships]);
useEffect(() => {
const handleReactionEvent = (event: MatrixEvent): void => {
const sender = event.getSender();
if (!sender) {
// Skip any event without a sender.
return;
}
if (event.getType() === EventType.Reaction) {
// TODO: check if target of reaction is a call membership event
const content = event.getContent() as ReactionEventContent;
if (content?.["m.relates_to"].key === "🖐️") {
addRaisedHand(sender, new Date(event.localTimestamp));
}
} else if (event.getType() === EventType.RoomRedaction) {
// TODO: check target of redaction event
removeRaisedHand(sender);
}
};
room.on(MatrixRoomEvent.Timeline, handleReactionEvent);
room.on(MatrixRoomEvent.Redaction, handleReactionEvent);
return (): void => {
room.off(MatrixRoomEvent.Timeline, handleReactionEvent);
room.off(MatrixRoomEvent.Redaction, handleReactionEvent);
};
}, [room, addRaisedHand, removeRaisedHand]);
return (
<ReactionsContext.Provider
value={{
raisedHands,
raisedHandCount,
addRaisedHand,
removeRaisedHand,
supportsReactions,
myReactionId,
setMyReactionId,
}}
>
{children}
</ReactionsContext.Provider>
);
};