mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-31 07:00:26 +00:00
Begin moving over to membershipEvents
This commit is contained in:
@@ -224,26 +224,34 @@ export function ReactionToggleButton({
|
||||
const { raisedHands, lowerHand, reactions } = useReactions();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const userId = client.getUserId()!;
|
||||
const isHandRaised = !!raisedHands[userId];
|
||||
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||
const [showReactionsMenu, setShowReactionsMenu] = useState(false);
|
||||
const [errorText, setErrorText] = useState<string>();
|
||||
|
||||
const myMembershipEventId = useMemo(
|
||||
() =>
|
||||
memberships.find(
|
||||
(m) =>
|
||||
m.sender === client.getUserId() &&
|
||||
m.deviceId === client.getDeviceId(),
|
||||
),
|
||||
[memberships],
|
||||
)?.eventId;
|
||||
|
||||
useEffect(() => {
|
||||
// Clear whenever the reactions menu state changes.
|
||||
setErrorText(undefined);
|
||||
}, [showReactionsMenu]);
|
||||
|
||||
const canReact = !reactions[userId];
|
||||
const canReact = !reactions[myMembershipEventId ?? ""];
|
||||
const isHandRaised = !!raisedHands[myMembershipEventId ?? ""];
|
||||
|
||||
const sendRelation = useCallback(
|
||||
async (reaction: ReactionOption) => {
|
||||
try {
|
||||
const myMembership = memberships.find((m) => m.sender === userId);
|
||||
if (!myMembership?.eventId) {
|
||||
throw new Error("Cannot find own membership event");
|
||||
if (!myMembershipEventId) {
|
||||
throw Error("Could not find own membership event");
|
||||
}
|
||||
const parentEventId = myMembership.eventId;
|
||||
setBusy(true);
|
||||
await client.sendEvent(
|
||||
rtcSession.room.roomId,
|
||||
@@ -251,7 +259,7 @@ export function ReactionToggleButton({
|
||||
{
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: parentEventId,
|
||||
event_id: myMembershipEventId,
|
||||
},
|
||||
emoji: reaction.emoji,
|
||||
name: reaction.name,
|
||||
@@ -266,7 +274,7 @@ export function ReactionToggleButton({
|
||||
setBusy(false);
|
||||
}
|
||||
},
|
||||
[memberships, client, userId, rtcSession],
|
||||
[myMembershipEventId, client, rtcSession],
|
||||
);
|
||||
|
||||
const toggleRaisedHand = useCallback(() => {
|
||||
@@ -281,11 +289,9 @@ export function ReactionToggleButton({
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const myMembership = memberships.find((m) => m.sender === userId);
|
||||
if (!myMembership?.eventId) {
|
||||
throw new Error("Cannot find own membership event");
|
||||
if (!myMembershipEventId) {
|
||||
throw Error("Could not find own membership event");
|
||||
}
|
||||
const parentEventId = myMembership.eventId;
|
||||
setBusy(true);
|
||||
const reaction = await client.sendEvent(
|
||||
rtcSession.room.roomId,
|
||||
@@ -293,7 +299,7 @@ export function ReactionToggleButton({
|
||||
{
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: parentEventId,
|
||||
event_id: myMembershipEventId,
|
||||
key: "🖐️",
|
||||
},
|
||||
},
|
||||
@@ -314,10 +320,9 @@ export function ReactionToggleButton({
|
||||
}, [
|
||||
client,
|
||||
isHandRaised,
|
||||
memberships,
|
||||
myMembershipEventId,
|
||||
lowerHand,
|
||||
rtcSession.room.roomId,
|
||||
userId,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
|
||||
const memberUserIdAlice = "@alice:example.org";
|
||||
const memberEventAlice = "$membership-alice:example.org";
|
||||
const memberEventAlice2 = "$membership-alice2:example.org";
|
||||
const memberUserIdBob = "@bob:example.org";
|
||||
const memberEventBob = "$membership-bob:example.org";
|
||||
|
||||
@@ -28,6 +29,8 @@ const membership: Record<string, string> = {
|
||||
[memberEventAlice]: memberUserIdAlice,
|
||||
[memberEventBob]: memberUserIdBob,
|
||||
"$membership-charlie:example.org": "@charlie:example.org",
|
||||
// Second device
|
||||
[memberEventAlice2]: memberEventAlice,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -42,9 +45,9 @@ const TestComponent: FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<ul>
|
||||
{Object.entries(raisedHands).map(([userId, date]) => (
|
||||
<li key={userId}>
|
||||
<span>{userId}</span>
|
||||
{Object.entries(raisedHands).map(([membershipEventId, date]) => (
|
||||
<li key={membershipEventId}>
|
||||
<span>{membershipEventId}</span>
|
||||
<time>{date.getTime()}</time>
|
||||
</li>
|
||||
))}
|
||||
@@ -170,4 +173,17 @@ describe("useReactions", () => {
|
||||
await act(() => room.testSendHandRaise(memberEventAlice, memberUserIdBob));
|
||||
expect(queryByRole("list")?.children).to.have.lengthOf(0);
|
||||
});
|
||||
test("handles multiple membership event reactions for the same sender", () => {
|
||||
const room = new MockRoom(memberUserIdAlice, [
|
||||
createHandRaisedReaction(memberEventAlice, membership),
|
||||
createHandRaisedReaction(memberEventAlice2, membership),
|
||||
]);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { queryByRole } = render(
|
||||
<TestReactionsWrapper rtcSession={rtcSession}>
|
||||
<TestComponent />
|
||||
</TestReactionsWrapper>,
|
||||
);
|
||||
expect(queryByRole("list")?.children).to.have.lengthOf(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,9 +48,9 @@ const ReactionsContext = createContext<ReactionsContextType | undefined>(
|
||||
|
||||
interface RaisedHandInfo {
|
||||
/**
|
||||
* Call membership event that was reacted to.
|
||||
* The sender who sent the raised hand.
|
||||
*/
|
||||
membershipEventId: string;
|
||||
sender: string;
|
||||
/**
|
||||
* Event ID of the reaction itself.
|
||||
*/
|
||||
@@ -71,6 +71,8 @@ export const useReactions = (): ReactionsContextType => {
|
||||
return context;
|
||||
};
|
||||
|
||||
type MembershipEventId = string;
|
||||
|
||||
/**
|
||||
* Provider that handles raised hand reactions for a given `rtcSession`.
|
||||
*/
|
||||
@@ -82,7 +84,7 @@ export const ReactionsProvider = ({
|
||||
rtcSession: MatrixRTCSession;
|
||||
}): JSX.Element => {
|
||||
const [raisedHands, setRaisedHands] = useState<
|
||||
Record<string, RaisedHandInfo>
|
||||
Record<MembershipEventId, RaisedHandInfo>
|
||||
>({});
|
||||
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||
const clientState = useClientState();
|
||||
@@ -91,29 +93,36 @@ export const ReactionsProvider = ({
|
||||
const room = rtcSession.room;
|
||||
const myUserId = room.client.getUserId();
|
||||
|
||||
const [reactions, setReactions] = useState<Record<string, ReactionOption>>(
|
||||
{},
|
||||
);
|
||||
const [reactions, setReactions] = useState<
|
||||
Record<MembershipEventId, ReactionOption>
|
||||
>({});
|
||||
|
||||
// Reduce the data down for the consumers.
|
||||
const resultRaisedHands = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
Object.entries(raisedHands).map(([uid, data]) => [uid, data.time]),
|
||||
Object.entries(raisedHands).map(([membershipEventId, data]) => [
|
||||
membershipEventId,
|
||||
data.time,
|
||||
]),
|
||||
),
|
||||
[raisedHands],
|
||||
);
|
||||
|
||||
const addRaisedHand = useCallback((userId: string, info: RaisedHandInfo) => {
|
||||
setRaisedHands((prevRaisedHands) => ({
|
||||
...prevRaisedHands,
|
||||
[userId]: info,
|
||||
}));
|
||||
}, []);
|
||||
const addRaisedHand = useCallback(
|
||||
(membershipEventId: string, info: RaisedHandInfo) => {
|
||||
setRaisedHands((prevRaisedHands) => ({
|
||||
...prevRaisedHands,
|
||||
[membershipEventId]: info,
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const removeRaisedHand = useCallback((userId: string) => {
|
||||
const removeRaisedHand = useCallback((membershipEventId: string) => {
|
||||
setRaisedHands(
|
||||
({ [userId]: _removed, ...remainingRaisedHands }) => remainingRaisedHands,
|
||||
({ [membershipEventId]: _removed, ...remainingRaisedHands }) =>
|
||||
remainingRaisedHands,
|
||||
);
|
||||
}, []);
|
||||
|
||||
@@ -138,11 +147,10 @@ export const ReactionsProvider = ({
|
||||
);
|
||||
};
|
||||
|
||||
// Remove any raised hands for users no longer joined to the call.
|
||||
for (const userId of Object.keys(raisedHands).filter(
|
||||
(rhId) => !memberships.find((u) => u.sender == rhId),
|
||||
for (const previousMemberEventId of Object.keys(raisedHands).filter(
|
||||
(eventId) => !memberships.some((m) => m.eventId === eventId),
|
||||
)) {
|
||||
removeRaisedHand(userId);
|
||||
removeRaisedHand(previousMemberEventId);
|
||||
}
|
||||
|
||||
// For each member in the call, check to see if a reaction has
|
||||
@@ -151,22 +159,14 @@ export const ReactionsProvider = ({
|
||||
if (!m.sender || !m.eventId) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
raisedHands[m.sender] &&
|
||||
raisedHands[m.sender].membershipEventId !== m.eventId
|
||||
) {
|
||||
// Membership event for sender has changed since the hand
|
||||
// was raised, reset.
|
||||
removeRaisedHand(m.sender);
|
||||
}
|
||||
const reaction = getLastReactionEvent(m.eventId, m.sender);
|
||||
if (reaction) {
|
||||
const eventId = reaction?.getId();
|
||||
if (!eventId) {
|
||||
continue;
|
||||
}
|
||||
addRaisedHand(m.sender, {
|
||||
membershipEventId: m.eventId,
|
||||
addRaisedHand(m.eventId, {
|
||||
sender: m.sender,
|
||||
reactionEventId: eventId,
|
||||
time: new Date(reaction.localTimestamp),
|
||||
});
|
||||
@@ -241,19 +241,21 @@ export const ReactionsProvider = ({
|
||||
};
|
||||
|
||||
setReactions((reactions) => {
|
||||
if (reactions[sender]) {
|
||||
if (reactions[membershipEventId]) {
|
||||
// We've still got a reaction from this user, ignore it to prevent spamming
|
||||
return reactions;
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
// Clear the reaction after some time.
|
||||
setReactions(({ [sender]: _unused, ...remaining }) => remaining);
|
||||
setReactions(
|
||||
({ [membershipEventId]: _unused, ...remaining }) => remaining,
|
||||
);
|
||||
reactionTimeouts.delete(timeout);
|
||||
}, REACTION_ACTIVE_TIME_MS);
|
||||
reactionTimeouts.add(timeout);
|
||||
return {
|
||||
...reactions,
|
||||
[sender]: reaction,
|
||||
[membershipEventId]: reaction,
|
||||
};
|
||||
});
|
||||
} else if (event.getType() === EventType.Reaction) {
|
||||
@@ -274,9 +276,9 @@ export const ReactionsProvider = ({
|
||||
}
|
||||
|
||||
if (content?.["m.relates_to"].key === "🖐️") {
|
||||
addRaisedHand(sender, {
|
||||
addRaisedHand(membershipEventId, {
|
||||
reactionEventId,
|
||||
membershipEventId,
|
||||
sender,
|
||||
time: new Date(event.localTimestamp),
|
||||
});
|
||||
}
|
||||
@@ -316,11 +318,22 @@ export const ReactionsProvider = ({
|
||||
latestRaisedHands,
|
||||
]);
|
||||
|
||||
const myMembershipEventId = useMemo(
|
||||
() =>
|
||||
memberships.find(
|
||||
(m) =>
|
||||
clientState?.state === "valid" &&
|
||||
m.sender === clientState.authenticated?.client.getUserId() &&
|
||||
m.deviceId === clientState.authenticated?.client.getDeviceId(),
|
||||
),
|
||||
[memberships],
|
||||
)?.eventId;
|
||||
|
||||
const lowerHand = useCallback(async () => {
|
||||
if (!myUserId || !raisedHands[myUserId]) {
|
||||
if (!myMembershipEventId || !raisedHands[myMembershipEventId]) {
|
||||
return;
|
||||
}
|
||||
const myReactionId = raisedHands[myUserId].reactionEventId;
|
||||
const myReactionId = raisedHands[myMembershipEventId].reactionEventId;
|
||||
if (!myReactionId) {
|
||||
logger.warn(`Hand raised but no reaction event to redact!`);
|
||||
return;
|
||||
@@ -331,7 +344,7 @@ export const ReactionsProvider = ({
|
||||
} catch (ex) {
|
||||
logger.error("Failed to redact reaction event", myReactionId, ex);
|
||||
}
|
||||
}, [myUserId, raisedHands, rtcSession, room]);
|
||||
}, [myMembershipEventId, raisedHands, rtcSession, room]);
|
||||
|
||||
return (
|
||||
<ReactionsContext.Provider
|
||||
|
||||
@@ -45,6 +45,7 @@ export class MockRTCSession extends EventEmitter {
|
||||
public memberships: {
|
||||
sender: string;
|
||||
eventId: string;
|
||||
deviceId: string;
|
||||
createdTs: () => Date;
|
||||
}[];
|
||||
|
||||
@@ -56,6 +57,7 @@ export class MockRTCSession extends EventEmitter {
|
||||
this.memberships = Object.entries(membership).map(([eventId, sender]) => ({
|
||||
sender,
|
||||
eventId,
|
||||
deviceId: randomUUID(),
|
||||
createdTs: (): Date => new Date(),
|
||||
}));
|
||||
}
|
||||
@@ -69,6 +71,7 @@ export class MockRTCSession extends EventEmitter {
|
||||
this.memberships.push({
|
||||
sender,
|
||||
eventId: `!fake-${randomUUID()}:event`,
|
||||
deviceId: randomUUID(),
|
||||
createdTs: (): Date => new Date(),
|
||||
});
|
||||
this.emit(MatrixRTCSessionEvent.MembershipsChanged);
|
||||
@@ -118,6 +121,7 @@ export class MockRoom extends EventEmitter {
|
||||
|
||||
public constructor(
|
||||
private readonly ownUserId: string,
|
||||
private readonly ownDeviceId: string,
|
||||
private readonly existingRelations: MatrixEvent[] = [],
|
||||
) {
|
||||
super();
|
||||
@@ -126,6 +130,7 @@ export class MockRoom extends EventEmitter {
|
||||
public get client(): MatrixClient {
|
||||
return {
|
||||
getUserId: (): string => this.ownUserId,
|
||||
getDeviceId: (): string => this.ownDeviceId,
|
||||
sendEvent: async (
|
||||
...props: Parameters<MatrixClient["sendEvent"]>
|
||||
): ReturnType<MatrixClient["sendEvent"]> => {
|
||||
|
||||
Reference in New Issue
Block a user