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