Begin moving over to membershipEvents

This commit is contained in:
Half-Shot
2024-11-08 17:04:10 +00:00
parent 1a95c576e0
commit dbad2b3902
4 changed files with 94 additions and 55 deletions

View File

@@ -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 (

View File

@@ -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);
});
});

View File

@@ -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

View File

@@ -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"]> => {