mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-31 07:00:26 +00:00
Tie up last bits of useReactions
This commit is contained in:
@@ -29,6 +29,9 @@ import { useReactions } from "../useReactions";
|
||||
import styles from "./ReactionToggleButton.module.css";
|
||||
import { ReactionOption, ReactionSet, ReactionsRowSize } from "../reactions";
|
||||
import { Modal } from "../Modal";
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
import { useObservableState } from "observable-hooks";
|
||||
import { map } from "rxjs";
|
||||
|
||||
interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||
raised: boolean;
|
||||
@@ -158,22 +161,27 @@ export function ReactionPopupMenu({
|
||||
}
|
||||
|
||||
interface ReactionToggleButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||
userId: string;
|
||||
identifier: string;
|
||||
vm: CallViewModel;
|
||||
}
|
||||
|
||||
export function ReactionToggleButton({
|
||||
userId,
|
||||
identifier,
|
||||
vm,
|
||||
...props
|
||||
}: ReactionToggleButtonProps): ReactNode {
|
||||
const { t } = useTranslation();
|
||||
const { raisedHands, toggleRaisedHand, sendReaction, reactions } =
|
||||
useReactions();
|
||||
const { toggleRaisedHand, sendReaction } = useReactions();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [showReactionsMenu, setShowReactionsMenu] = useState(false);
|
||||
const [errorText, setErrorText] = useState<string>();
|
||||
|
||||
const isHandRaised = !!raisedHands[userId];
|
||||
const canReact = !reactions[userId];
|
||||
const isHandRaised = useObservableState(
|
||||
vm.handsRaised.pipe(map((v) => !!v[identifier])),
|
||||
);
|
||||
const canReact = useObservableState(
|
||||
vm.reactions.pipe(map((v) => !!v[identifier])),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Clear whenever the reactions menu state changes.
|
||||
@@ -219,7 +227,7 @@ export function ReactionToggleButton({
|
||||
<InnerButton
|
||||
disabled={busy}
|
||||
onClick={() => setShowReactionsMenu((show) => !show)}
|
||||
raised={isHandRaised}
|
||||
raised={!!isHandRaised}
|
||||
open={showReactionsMenu}
|
||||
{...props}
|
||||
/>
|
||||
@@ -233,8 +241,8 @@ export function ReactionToggleButton({
|
||||
>
|
||||
<ReactionPopupMenu
|
||||
errorText={errorText}
|
||||
isHandRaised={isHandRaised}
|
||||
canReact={!busy && canReact}
|
||||
isHandRaised={!!isHandRaised}
|
||||
canReact={!busy && !!canReact}
|
||||
sendReaction={(reaction) => void sendRelation(reaction)}
|
||||
toggleRaisedHand={wrappedToggleRaisedHand}
|
||||
/>
|
||||
|
||||
@@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ReactNode, useDeferredValue, useEffect, useMemo } from "react";
|
||||
import { filter, interval, map, scan, throttle } from "rxjs";
|
||||
import { ReactNode, useEffect } from "react";
|
||||
import { filter, interval, throttle } from "rxjs";
|
||||
|
||||
import { CallViewModel } from "../state/CallViewModel";
|
||||
import joinCallSoundMp3 from "../sound/join_call.mp3";
|
||||
@@ -17,7 +17,6 @@ import handSoundOgg from "../sound/raise_hand.ogg?url";
|
||||
import handSoundMp3 from "../sound/raise_hand.mp3?url";
|
||||
import { useAudioContext } from "../useAudioContext";
|
||||
import { prefetchSounds } from "../soundUtils";
|
||||
import { useReactions } from "../useReactions";
|
||||
import { useLatest } from "../useLatest";
|
||||
|
||||
// Do not play any sounds if the participant count has exceeded this
|
||||
|
||||
@@ -544,9 +544,10 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
if (supportsReactions) {
|
||||
buttons.push(
|
||||
<ReactionToggleButton
|
||||
vm={vm}
|
||||
key="raise_hand"
|
||||
className={styles.raiseHand}
|
||||
userId={client.getUserId()!}
|
||||
identifier={`${client.getUserId()}:${client.getDeviceId()}`}
|
||||
onTouchEnd={onControlsTouchEnd}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -1118,7 +1118,7 @@ export class CallViewModel extends ViewModel {
|
||||
);
|
||||
|
||||
public readonly handsRaised = new Subject<Record<string, Date>>();
|
||||
private readonly reactions = new Subject<Record<string, ReactionOption>>();
|
||||
public readonly reactions = new Subject<Record<string, ReactionOption>>();
|
||||
|
||||
public updateReactions(data: ReturnType<typeof useReactions>) {
|
||||
this.handsRaised.next(data.raisedHands);
|
||||
|
||||
@@ -37,8 +37,14 @@ import {
|
||||
import { useLatest } from "./useLatest";
|
||||
|
||||
interface ReactionsContextType {
|
||||
/**
|
||||
* identifier (userId:deviceId => Date)
|
||||
*/
|
||||
raisedHands: Record<string, Date>;
|
||||
supportsReactions: boolean;
|
||||
/**
|
||||
* reactions (userId:deviceId => Date)
|
||||
*/
|
||||
reactions: Record<string, ReactionOption>;
|
||||
toggleRaisedHand: () => Promise<void>;
|
||||
sendReaction: (reaction: ReactionOption) => Promise<void>;
|
||||
@@ -92,6 +98,24 @@ export const ReactionsProvider = ({
|
||||
clientState?.state === "valid" && clientState.supportedFeatures.reactions;
|
||||
const room = rtcSession.room;
|
||||
const myUserId = room.client.getUserId();
|
||||
const myDeviceId = room.client.getDeviceId();
|
||||
|
||||
const latestMemberships = useLatest(memberships);
|
||||
const latestRaisedHands = useLatest(raisedHands);
|
||||
|
||||
const myMembershipEvent = useMemo(
|
||||
() =>
|
||||
memberships.find(
|
||||
(m) => m.sender === myUserId && m.deviceId === myDeviceId,
|
||||
)?.eventId,
|
||||
[memberships, myUserId],
|
||||
);
|
||||
const myMembershipIdentifier = useMemo(() => {
|
||||
const membership = memberships.find((m) => m.sender === myUserId);
|
||||
return membership
|
||||
? `${membership.sender}:${membership.deviceId}`
|
||||
: undefined;
|
||||
}, [memberships, myUserId]);
|
||||
|
||||
const [reactions, setReactions] = useState<Record<string, ReactionOption>>(
|
||||
{},
|
||||
@@ -177,21 +201,8 @@ export const ReactionsProvider = ({
|
||||
// Ignoring raisedHands here because we don't want to trigger each time the raised
|
||||
// hands set is updated.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [room, memberships, myUserId, addRaisedHand, removeRaisedHand]);
|
||||
}, [room, memberships, addRaisedHand, removeRaisedHand]);
|
||||
|
||||
const latestMemberships = useLatest(memberships);
|
||||
const latestRaisedHands = useLatest(raisedHands);
|
||||
|
||||
const myMembershipEvent = useMemo(
|
||||
() => memberships.find((m) => m.sender === myUserId)?.eventId,
|
||||
[memberships, myUserId],
|
||||
);
|
||||
const myMembershipIdentifier = useMemo(() => {
|
||||
const membership = memberships.find((m) => m.sender === myUserId);
|
||||
return membership
|
||||
? `${membership.sender}:${membership.deviceId}`
|
||||
: undefined;
|
||||
}, [memberships, myUserId]);
|
||||
// This effect handles any *live* reaction/redactions in the room.
|
||||
useEffect(() => {
|
||||
const reactionTimeouts = new Set<number>();
|
||||
@@ -215,18 +226,18 @@ export const ReactionsProvider = ({
|
||||
const content: ECallReactionEventContent = event.getContent();
|
||||
|
||||
const membershipEventId = content?.["m.relates_to"]?.event_id;
|
||||
const membershipEvent = latestMemberships.current.find(
|
||||
(e) => e.eventId === membershipEventId && e.sender === sender,
|
||||
);
|
||||
// Check to see if this reaction was made to a membership event (and the
|
||||
// sender of the reaction matches the membership)
|
||||
if (
|
||||
!latestMemberships.current.some(
|
||||
(e) => e.eventId === membershipEventId && e.sender === sender,
|
||||
)
|
||||
) {
|
||||
if (!membershipEvent) {
|
||||
logger.warn(
|
||||
`Reaction target was not a membership event for ${sender}, ignoring`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const identifier = `${membershipEvent.sender}:${membershipEvent.deviceId}`;
|
||||
|
||||
if (!content.emoji) {
|
||||
logger.warn(`Reaction had no emoji from ${reactionEventId}`);
|
||||
@@ -256,19 +267,21 @@ export const ReactionsProvider = ({
|
||||
};
|
||||
|
||||
setReactions((reactions) => {
|
||||
if (reactions[sender]) {
|
||||
if (reactions[identifier]) {
|
||||
// We've still got a reaction from this user, ignore it to prevent spamming
|
||||
return reactions;
|
||||
}
|
||||
const timeout = window.setTimeout(() => {
|
||||
// Clear the reaction after some time.
|
||||
setReactions(({ [sender]: _unused, ...remaining }) => remaining);
|
||||
setReactions(
|
||||
({ [identifier]: _unused, ...remaining }) => remaining,
|
||||
);
|
||||
reactionTimeouts.delete(timeout);
|
||||
}, REACTION_ACTIVE_TIME_MS);
|
||||
reactionTimeouts.add(timeout);
|
||||
return {
|
||||
...reactions,
|
||||
[sender]: reaction,
|
||||
[identifier]: reaction,
|
||||
};
|
||||
});
|
||||
} else if (event.getType() === EventType.Reaction) {
|
||||
@@ -380,7 +393,7 @@ export const ReactionsProvider = ({
|
||||
|
||||
const sendReaction = useCallback(
|
||||
async (reaction: ReactionOption) => {
|
||||
if (!myUserId || reactions[myUserId]) {
|
||||
if (!myMembershipIdentifier || !reactions[myMembershipIdentifier]) {
|
||||
// We're still reacting
|
||||
return;
|
||||
}
|
||||
@@ -400,7 +413,7 @@ export const ReactionsProvider = ({
|
||||
},
|
||||
);
|
||||
},
|
||||
[myMembershipEvent, reactions, room, myUserId, rtcSession],
|
||||
[myMembershipEvent, reactions, room, myMembershipIdentifier, rtcSession],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user