mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-05 19:59:20 +00:00
Major refactor to support various state problems.
This commit is contained in:
@@ -110,7 +110,11 @@ export function RaiseHandToggleButton({
|
|||||||
.then((reaction) => {
|
.then((reaction) => {
|
||||||
logger.debug("Sent raise hand event", reaction.event_id);
|
logger.debug("Sent raise hand event", reaction.event_id);
|
||||||
setMyReactionId(reaction.event_id);
|
setMyReactionId(reaction.event_id);
|
||||||
addRaisedHand(userId, parentEventId, new Date());
|
addRaisedHand(userId, {
|
||||||
|
membershipEventId: parentEventId,
|
||||||
|
reactionEventId: reaction.event_id,
|
||||||
|
time: new Date(),
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
logger.error("Failed to send reaction event", e);
|
logger.error("Failed to send reaction event", e);
|
||||||
|
|||||||
@@ -174,7 +174,8 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
connState,
|
connState,
|
||||||
onShareClick,
|
onShareClick,
|
||||||
}) => {
|
}) => {
|
||||||
const { supportsReactions, raisedHandCount } = useReactions();
|
const { supportsReactions, raisedHands } = useReactions();
|
||||||
|
const raisedHandCount = Object.keys(raisedHands).length;
|
||||||
const [previousRaisedHandCount, setPreviousRaisedHandCount] =
|
const [previousRaisedHandCount, setPreviousRaisedHandCount] =
|
||||||
useState(raisedHandCount);
|
useState(raisedHandCount);
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ test("GridTile is accessible", async () => {
|
|||||||
room: {
|
room: {
|
||||||
on: () => {},
|
on: () => {},
|
||||||
off: () => {},
|
off: () => {},
|
||||||
|
client: {
|
||||||
|
getUserId: () => null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
memberships: [],
|
memberships: [],
|
||||||
} as unknown as MatrixRTCSession;
|
} as unknown as MatrixRTCSession;
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { FC, ReactNode } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
import {
|
||||||
|
MatrixRTCSession,
|
||||||
|
MatrixRTCSessionEvent,
|
||||||
|
} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
import {
|
import {
|
||||||
EventTimeline,
|
EventTimeline,
|
||||||
EventTimelineSet,
|
EventTimelineSet,
|
||||||
@@ -43,25 +46,44 @@ const TestComponent: FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TestComponentWrapper = ({ room }: { room: MockRoom }): ReactNode => {
|
const TestComponentWrapper = ({
|
||||||
const fakeRtcSession = {
|
rtcSession,
|
||||||
on: () => {},
|
}: {
|
||||||
off: () => {},
|
rtcSession: MockRTCSession;
|
||||||
room,
|
}): ReactNode => {
|
||||||
memberships: membership.map((sender) => ({
|
|
||||||
sender,
|
|
||||||
eventId: "!fake:event",
|
|
||||||
createdTs: (): Date => new Date(),
|
|
||||||
})),
|
|
||||||
} as unknown as MatrixRTCSession;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactionsProvider rtcSession={fakeRtcSession}>
|
<ReactionsProvider rtcSession={rtcSession as unknown as MatrixRTCSession}>
|
||||||
<TestComponent />
|
<TestComponent />
|
||||||
</ReactionsProvider>
|
</ReactionsProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export class MockRTCSession extends EventEmitter {
|
||||||
|
public memberships = membership.map((sender) => ({
|
||||||
|
sender,
|
||||||
|
eventId: `!fake-${randomUUID()}:event`,
|
||||||
|
createdTs: (): Date => new Date(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
public constructor(public readonly room: MockRoom) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public testRemoveMember(userId: string) {
|
||||||
|
this.memberships = this.memberships.filter((u) => u.sender !== userId);
|
||||||
|
this.emit(MatrixRTCSessionEvent.MembershipsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public testAddMember(sender: string) {
|
||||||
|
this.memberships.push({
|
||||||
|
sender,
|
||||||
|
eventId: `!fake-${randomUUID()}:event`,
|
||||||
|
createdTs: (): Date => new Date(),
|
||||||
|
});
|
||||||
|
this.emit(MatrixRTCSessionEvent.MembershipsChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createReaction(sender: string): MatrixEvent {
|
function createReaction(sender: string): MatrixEvent {
|
||||||
return new MatrixEvent({
|
return new MatrixEvent({
|
||||||
sender,
|
sender,
|
||||||
@@ -121,29 +143,33 @@ export class MockRoom extends EventEmitter {
|
|||||||
|
|
||||||
describe("useReactions", () => {
|
describe("useReactions", () => {
|
||||||
test("starts with an empty list", () => {
|
test("starts with an empty list", () => {
|
||||||
const room = new MockRoom();
|
const rtcSession = new MockRTCSession(new MockRoom());
|
||||||
const { queryByRole } = render(<TestComponentWrapper room={room} />);
|
const { queryByRole } = render(
|
||||||
|
<TestComponentWrapper rtcSession={rtcSession} />,
|
||||||
|
);
|
||||||
expect(queryByRole("list")?.children).to.have.lengthOf(0);
|
expect(queryByRole("list")?.children).to.have.lengthOf(0);
|
||||||
});
|
});
|
||||||
test("handles incoming raised hand", () => {
|
test("handles incoming raised hand", () => {
|
||||||
const room = new MockRoom();
|
const room = new MockRoom();
|
||||||
|
const rtcSession = new MockRTCSession(room);
|
||||||
const { queryByRole, rerender } = render(
|
const { queryByRole, rerender } = render(
|
||||||
<TestComponentWrapper room={room} />,
|
<TestComponentWrapper rtcSession={rtcSession} />,
|
||||||
);
|
);
|
||||||
room.testSendReaction("@foo:bar");
|
room.testSendReaction("@foo:bar");
|
||||||
rerender(<TestComponentWrapper room={room} />);
|
rerender(<TestComponentWrapper rtcSession={rtcSession} />);
|
||||||
expect(queryByRole("list")?.children).to.have.lengthOf(1);
|
expect(queryByRole("list")?.children).to.have.lengthOf(1);
|
||||||
room.testSendReaction("@baz:bar");
|
room.testSendReaction("@baz:bar");
|
||||||
rerender(<TestComponentWrapper room={room} />);
|
rerender(<TestComponentWrapper rtcSession={rtcSession} />);
|
||||||
expect(queryByRole("list")?.children).to.have.lengthOf(2);
|
expect(queryByRole("list")?.children).to.have.lengthOf(2);
|
||||||
});
|
});
|
||||||
test("handles incoming unraised hand", () => {
|
test("handles incoming unraised hand", () => {
|
||||||
const room = new MockRoom();
|
const room = new MockRoom();
|
||||||
|
const rtcSession = new MockRTCSession(room);
|
||||||
const { queryByRole, rerender } = render(
|
const { queryByRole, rerender } = render(
|
||||||
<TestComponentWrapper room={room} />,
|
<TestComponentWrapper rtcSession={rtcSession} />,
|
||||||
);
|
);
|
||||||
room.testSendReaction("@foo:bar");
|
room.testSendReaction("@foo:bar");
|
||||||
rerender(<TestComponentWrapper room={room} />);
|
rerender(<TestComponentWrapper rtcSession={rtcSession} />);
|
||||||
expect(queryByRole("list")?.children).to.have.lengthOf(1);
|
expect(queryByRole("list")?.children).to.have.lengthOf(1);
|
||||||
room.emit(
|
room.emit(
|
||||||
RoomEvent.Redaction,
|
RoomEvent.Redaction,
|
||||||
@@ -151,12 +177,38 @@ describe("useReactions", () => {
|
|||||||
room,
|
room,
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
rerender(<TestComponentWrapper room={room} />);
|
rerender(<TestComponentWrapper rtcSession={rtcSession} />);
|
||||||
expect(queryByRole("list")?.children).to.have.lengthOf(0);
|
expect(queryByRole("list")?.children).to.have.lengthOf(0);
|
||||||
});
|
});
|
||||||
test("handles loading events from cold", () => {
|
test("handles loading events from cold", () => {
|
||||||
const room = new MockRoom([createReaction(membership[0])]);
|
const room = new MockRoom([createReaction(membership[0])]);
|
||||||
const { queryByRole } = render(<TestComponentWrapper room={room} />);
|
const rtcSession = new MockRTCSession(room);
|
||||||
|
const { queryByRole } = render(
|
||||||
|
<TestComponentWrapper rtcSession={rtcSession} />,
|
||||||
|
);
|
||||||
expect(queryByRole("list")?.children).to.have.lengthOf(1);
|
expect(queryByRole("list")?.children).to.have.lengthOf(1);
|
||||||
});
|
});
|
||||||
|
test.only("will remove reaction when a member leaves the call", () => {
|
||||||
|
const room = new MockRoom([createReaction(membership[0])]);
|
||||||
|
const rtcSession = new MockRTCSession(room);
|
||||||
|
const { queryByRole, rerender } = render(
|
||||||
|
<TestComponentWrapper rtcSession={rtcSession} />,
|
||||||
|
);
|
||||||
|
expect(queryByRole("list")?.children).to.have.lengthOf(1);
|
||||||
|
rtcSession.testRemoveMember(membership[0]);
|
||||||
|
rerender(<TestComponentWrapper rtcSession={rtcSession} />);
|
||||||
|
expect(queryByRole("list")?.children).to.have.lengthOf(0);
|
||||||
|
});
|
||||||
|
test("will remove reaction when a member joins via a new event", () => {
|
||||||
|
const room = new MockRoom([createReaction(membership[0])]);
|
||||||
|
const rtcSession = new MockRTCSession(room);
|
||||||
|
const { queryByRole, rerender } = render(
|
||||||
|
<TestComponentWrapper rtcSession={rtcSession} />,
|
||||||
|
);
|
||||||
|
expect(queryByRole("list")?.children).to.have.lengthOf(1);
|
||||||
|
rtcSession.testRemoveMember(membership[0]);
|
||||||
|
rtcSession.testAddMember(membership[0]);
|
||||||
|
rerender(<TestComponentWrapper rtcSession={rtcSession} />);
|
||||||
|
expect(queryByRole("list")?.children).to.have.lengthOf(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
|||||||
|
|
||||||
import { useMatrixRTCSessionMemberships } from "./useMatrixRTCSessionMemberships";
|
import { useMatrixRTCSessionMemberships } from "./useMatrixRTCSessionMemberships";
|
||||||
import { useClientState } from "./ClientContext";
|
import { useClientState } from "./ClientContext";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
interface ReactionsContextType {
|
interface ReactionsContextType {
|
||||||
raisedHands: Record<string, Date>;
|
raisedHands: Record<string, Date>;
|
||||||
raisedHandCount: number;
|
addRaisedHand: (userId: string, info: RaisedHandInfo) => void;
|
||||||
addRaisedHand: (userId: string, parentEventId: string, date: Date) => void;
|
|
||||||
removeRaisedHand: (userId: string) => void;
|
removeRaisedHand: (userId: string) => void;
|
||||||
supportsReactions: boolean;
|
supportsReactions: boolean;
|
||||||
myReactionId: string | null;
|
myReactionId: string | null;
|
||||||
@@ -40,6 +40,12 @@ const ReactionsContext = createContext<ReactionsContextType | undefined>(
|
|||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
interface RaisedHandInfo {
|
||||||
|
membershipEventId: string;
|
||||||
|
reactionEventId: string;
|
||||||
|
time: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export const useReactions = (): ReactionsContextType => {
|
export const useReactions = (): ReactionsContextType => {
|
||||||
const context = useContext(ReactionsContext);
|
const context = useContext(ReactionsContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
@@ -56,32 +62,23 @@ export const ReactionsProvider = ({
|
|||||||
rtcSession: MatrixRTCSession;
|
rtcSession: MatrixRTCSession;
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const [raisedHands, setRaisedHands] = useState<
|
const [raisedHands, setRaisedHands] = useState<
|
||||||
Record<
|
Record<string, RaisedHandInfo>
|
||||||
string,
|
|
||||||
{
|
|
||||||
time: Date;
|
|
||||||
parentEventId: string;
|
|
||||||
}
|
|
||||||
>
|
|
||||||
>({});
|
>({});
|
||||||
const [myReactionId, setMyReactionId] = useState<string | null>(null);
|
const [myReactionId, setMyReactionId] = useState<string | null>(null);
|
||||||
const [raisedHandCount, setRaisedHandCount] = useState(0);
|
|
||||||
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||||
const clientState = useClientState();
|
const clientState = useClientState();
|
||||||
const supportsReactions =
|
const supportsReactions =
|
||||||
clientState?.state === "valid" && clientState.supportedFeatures.reactions;
|
clientState?.state === "valid" && clientState.supportedFeatures.reactions;
|
||||||
const room = rtcSession.room;
|
const room = rtcSession.room;
|
||||||
|
|
||||||
|
const myUserId = room.client.getUserId();
|
||||||
|
|
||||||
const addRaisedHand = useCallback(
|
const addRaisedHand = useCallback(
|
||||||
(userId: string, parentEventId: string, time: Date) => {
|
(userId: string, info: RaisedHandInfo) => {
|
||||||
setRaisedHands({
|
setRaisedHands({
|
||||||
...raisedHands,
|
...raisedHands,
|
||||||
[userId]: {
|
[userId]: info,
|
||||||
time,
|
|
||||||
parentEventId,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setRaisedHandCount(Object.keys(raisedHands).length + 1);
|
|
||||||
},
|
},
|
||||||
[raisedHands],
|
[raisedHands],
|
||||||
);
|
);
|
||||||
@@ -89,11 +86,10 @@ export const ReactionsProvider = ({
|
|||||||
const removeRaisedHand = useCallback(
|
const removeRaisedHand = useCallback(
|
||||||
(userId: string) => {
|
(userId: string) => {
|
||||||
delete raisedHands[userId];
|
delete raisedHands[userId];
|
||||||
if (userId) {
|
if (userId === myUserId) {
|
||||||
setMyReactionId(null);
|
setMyReactionId(null);
|
||||||
}
|
}
|
||||||
setRaisedHands(raisedHands);
|
setRaisedHands({ ...raisedHands });
|
||||||
setRaisedHandCount(Object.keys(raisedHands).length);
|
|
||||||
},
|
},
|
||||||
[raisedHands],
|
[raisedHands],
|
||||||
);
|
);
|
||||||
@@ -110,13 +106,21 @@ export const ReactionsProvider = ({
|
|||||||
return allEvents.length > 0 ? allEvents[0] : undefined;
|
return allEvents.length > 0 ? allEvents[0] : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(memberships, raisedHands);
|
||||||
|
// 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),
|
||||||
|
)) {
|
||||||
|
removeRaisedHand(userId);
|
||||||
|
}
|
||||||
|
|
||||||
for (const m of memberships) {
|
for (const m of memberships) {
|
||||||
if (!m.sender || !m.eventId) {
|
if (!m.sender || !m.eventId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
raisedHands[m.sender] &&
|
raisedHands[m.sender] &&
|
||||||
raisedHands[m.sender].parentEventId !== m.eventId
|
raisedHands[m.sender].membershipEventId !== m.eventId
|
||||||
) {
|
) {
|
||||||
// Membership event for sender has changed.
|
// Membership event for sender has changed.
|
||||||
removeRaisedHand(m.sender);
|
removeRaisedHand(m.sender);
|
||||||
@@ -129,13 +133,19 @@ export const ReactionsProvider = ({
|
|||||||
if (reaction && reaction.getType() === EventType.Reaction) {
|
if (reaction && reaction.getType() === EventType.Reaction) {
|
||||||
const content = reaction.getContent() as ReactionEventContent;
|
const content = reaction.getContent() as ReactionEventContent;
|
||||||
if (content?.["m.relates_to"]?.key === "🖐️") {
|
if (content?.["m.relates_to"]?.key === "🖐️") {
|
||||||
addRaisedHand(m.sender, m.eventId, new Date(reaction.localTimestamp));
|
console.log("found key, raising hand", m.sender);
|
||||||
|
addRaisedHand(m.sender, {
|
||||||
|
membershipEventId: m.eventId,
|
||||||
|
reactionEventId: eventId,
|
||||||
|
time: new Date(reaction.localTimestamp),
|
||||||
|
});
|
||||||
if (m.sender === room.client.getUserId()) {
|
if (m.sender === room.client.getUserId()) {
|
||||||
setMyReactionId(eventId);
|
setMyReactionId(eventId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log("After", raisedHands);
|
||||||
// Deliberately ignoring addRaisedHand, raisedHands which was causing looping.
|
// Deliberately ignoring addRaisedHand, raisedHands which was causing looping.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [room, memberships]);
|
}, [room, memberships]);
|
||||||
@@ -143,23 +153,45 @@ export const ReactionsProvider = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleReactionEvent = (event: MatrixEvent): void => {
|
const handleReactionEvent = (event: MatrixEvent): void => {
|
||||||
const sender = event.getSender();
|
const sender = event.getSender();
|
||||||
if (!sender) {
|
const reactionEventId = event.getId();
|
||||||
// Skip any event without a sender.
|
if (!sender || !reactionEventId) {
|
||||||
|
// Skip any event without a sender or event ID.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.getType() === EventType.Reaction) {
|
if (event.getType() === EventType.Reaction) {
|
||||||
// TODO: check if target of reaction is a call membership event
|
|
||||||
const content = event.getContent() as ReactionEventContent;
|
const content = event.getContent() as ReactionEventContent;
|
||||||
if (content?.["m.relates_to"].key === "🖐️") {
|
const membershipEventId = content["m.relates_to"].event_id;
|
||||||
addRaisedHand(
|
|
||||||
sender,
|
if (
|
||||||
content["m.relates_to"].event_id,
|
!memberships.some(
|
||||||
new Date(event.localTimestamp),
|
(e) => e.eventId === membershipEventId && e.sender === sender,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
`Reaction target was not a membership event for ${sender}, ignoring`,
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content?.["m.relates_to"].key === "🖐️") {
|
||||||
|
addRaisedHand(sender, {
|
||||||
|
reactionEventId,
|
||||||
|
membershipEventId,
|
||||||
|
time: new Date(event.localTimestamp),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (event.getType() === EventType.RoomRedaction) {
|
} else if (event.getType() === EventType.RoomRedaction) {
|
||||||
// TODO: check target of redaction event
|
const targetEvent = event.event.redacts;
|
||||||
removeRaisedHand(sender);
|
const targetUser = Object.entries(raisedHands).find(
|
||||||
|
([u, r]) => r.reactionEventId === targetEvent,
|
||||||
|
)?.[0];
|
||||||
|
console.log(targetEvent, raisedHands);
|
||||||
|
if (!targetUser) {
|
||||||
|
// Reaction target was not for us, ignoring
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeRaisedHand(targetUser);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,19 +204,19 @@ export const ReactionsProvider = ({
|
|||||||
};
|
};
|
||||||
}, [room, addRaisedHand, removeRaisedHand]);
|
}, [room, addRaisedHand, removeRaisedHand]);
|
||||||
|
|
||||||
|
// 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(([uid, data]) => [uid, data.time]),
|
||||||
),
|
),
|
||||||
[raisedHands, raisedHandCount],
|
[raisedHands],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactionsContext.Provider
|
<ReactionsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
raisedHands: resultRaisedHands,
|
raisedHands: resultRaisedHands,
|
||||||
raisedHandCount,
|
|
||||||
addRaisedHand,
|
addRaisedHand,
|
||||||
removeRaisedHand,
|
removeRaisedHand,
|
||||||
supportsReactions,
|
supportsReactions,
|
||||||
|
|||||||
Reference in New Issue
Block a user