mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
Add tests for useReactionsReader
This commit is contained in:
@@ -198,10 +198,6 @@ export interface RaisedHandInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ReactionInfo {
|
export interface ReactionInfo {
|
||||||
/**
|
expireAfter: Date;
|
||||||
* ...
|
|
||||||
*/
|
|
||||||
ttl: number;
|
|
||||||
|
|
||||||
reactionOption: ReactionOption;
|
reactionOption: ReactionOption;
|
||||||
}
|
}
|
||||||
|
|||||||
512
src/reactions/useReactionsReader.test.tsx
Normal file
512
src/reactions/useReactionsReader.test.tsx
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { renderHook } from "@testing-library/react";
|
||||||
|
import { afterEach, test, vitest } from "vitest";
|
||||||
|
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||||
|
import {
|
||||||
|
RoomEvent as MatrixRoomEvent,
|
||||||
|
MatrixEvent,
|
||||||
|
type IRoomTimelineData,
|
||||||
|
EventType,
|
||||||
|
MatrixEventEvent,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import useReactionsReader, {
|
||||||
|
REACTION_ACTIVE_TIME_MS,
|
||||||
|
} from "./useReactionsReader";
|
||||||
|
import {
|
||||||
|
alice,
|
||||||
|
aliceRtcMember,
|
||||||
|
local,
|
||||||
|
localRtcMember,
|
||||||
|
} from "../utils/test-fixtures";
|
||||||
|
import { getBasicRTCSession } from "../utils/test-viewmodel";
|
||||||
|
import { withTestScheduler } from "../utils/test";
|
||||||
|
import { ElementCallReactionEventType, ReactionSet } from ".";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vitest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles a hand raised reaction", () => {
|
||||||
|
const { rtcSession } = getBasicRTCSession([local, alice]);
|
||||||
|
const reactionEventId = "$my_event_id:example.org";
|
||||||
|
const localTimestamp = new Date();
|
||||||
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
|
renderHook(() => {
|
||||||
|
const { raisedHands$ } = useReactionsReader(
|
||||||
|
rtcSession as unknown as MatrixRTCSession,
|
||||||
|
);
|
||||||
|
schedule("ab", {
|
||||||
|
a: () => {},
|
||||||
|
b: () => {
|
||||||
|
rtcSession.room.emit(
|
||||||
|
MatrixRoomEvent.Timeline,
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: rtcSession.room.roomId,
|
||||||
|
event_id: reactionEventId,
|
||||||
|
sender: localRtcMember.sender,
|
||||||
|
type: EventType.Reaction,
|
||||||
|
origin_server_ts: localTimestamp.getTime(),
|
||||||
|
content: {
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: localRtcMember.eventId,
|
||||||
|
key: "🖐️",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
rtcSession.room,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
{} as IRoomTimelineData,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expectObservable(raisedHands$).toBe("ab", {
|
||||||
|
a: {},
|
||||||
|
b: {
|
||||||
|
[`${localRtcMember.sender}:${localRtcMember.deviceId}`]: {
|
||||||
|
reactionEventId,
|
||||||
|
membershipEventId: localRtcMember.eventId,
|
||||||
|
time: localTimestamp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles a redaction", () => {
|
||||||
|
const { rtcSession } = getBasicRTCSession([local, alice]);
|
||||||
|
const reactionEventId = "$my_event_id:example.org";
|
||||||
|
const localTimestamp = new Date();
|
||||||
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
|
renderHook(() => {
|
||||||
|
const { raisedHands$ } = useReactionsReader(
|
||||||
|
rtcSession as unknown as MatrixRTCSession,
|
||||||
|
);
|
||||||
|
schedule("abc", {
|
||||||
|
a: () => {},
|
||||||
|
b: () => {
|
||||||
|
rtcSession.room.emit(
|
||||||
|
MatrixRoomEvent.Timeline,
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: rtcSession.room.roomId,
|
||||||
|
event_id: reactionEventId,
|
||||||
|
sender: localRtcMember.sender,
|
||||||
|
type: EventType.Reaction,
|
||||||
|
origin_server_ts: localTimestamp.getTime(),
|
||||||
|
content: {
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: localRtcMember.eventId,
|
||||||
|
key: "🖐️",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
rtcSession.room,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
{} as IRoomTimelineData,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
c: () => {
|
||||||
|
rtcSession.room.emit(
|
||||||
|
MatrixRoomEvent.Redaction,
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: rtcSession.room.roomId,
|
||||||
|
event_id: reactionEventId,
|
||||||
|
sender: localRtcMember.sender,
|
||||||
|
type: EventType.RoomRedaction,
|
||||||
|
redacts: reactionEventId,
|
||||||
|
}),
|
||||||
|
rtcSession.room,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expectObservable(raisedHands$).toBe("abc", {
|
||||||
|
a: {},
|
||||||
|
b: {
|
||||||
|
[`${localRtcMember.sender}:${localRtcMember.deviceId}`]: {
|
||||||
|
reactionEventId,
|
||||||
|
membershipEventId: localRtcMember.eventId,
|
||||||
|
time: localTimestamp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
c: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles waiting for event decryption", () => {
|
||||||
|
const { rtcSession } = getBasicRTCSession([local, alice]);
|
||||||
|
const reactionEventId = "$my_event_id:example.org";
|
||||||
|
const localTimestamp = new Date();
|
||||||
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
|
renderHook(() => {
|
||||||
|
const { raisedHands$ } = useReactionsReader(
|
||||||
|
rtcSession as unknown as MatrixRTCSession,
|
||||||
|
);
|
||||||
|
schedule("abc", {
|
||||||
|
a: () => {},
|
||||||
|
b: () => {
|
||||||
|
const encryptedEvent = new MatrixEvent({
|
||||||
|
room_id: rtcSession.room.roomId,
|
||||||
|
event_id: reactionEventId,
|
||||||
|
sender: localRtcMember.sender,
|
||||||
|
type: EventType.Reaction,
|
||||||
|
origin_server_ts: localTimestamp.getTime(),
|
||||||
|
content: {
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: localRtcMember.eventId,
|
||||||
|
key: "🖐️",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Should ignore encrypted events that are still encrypting
|
||||||
|
encryptedEvent["decryptionPromise"] = Promise.resolve();
|
||||||
|
rtcSession.room.emit(
|
||||||
|
MatrixRoomEvent.Timeline,
|
||||||
|
encryptedEvent,
|
||||||
|
rtcSession.room,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
{} as IRoomTimelineData,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
c: () => {
|
||||||
|
rtcSession.room.client.emit(
|
||||||
|
MatrixEventEvent.Decrypted,
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: rtcSession.room.roomId,
|
||||||
|
event_id: reactionEventId,
|
||||||
|
sender: localRtcMember.sender,
|
||||||
|
type: EventType.Reaction,
|
||||||
|
origin_server_ts: localTimestamp.getTime(),
|
||||||
|
content: {
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: localRtcMember.eventId,
|
||||||
|
key: "🖐️",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expectObservable(raisedHands$).toBe("a-c", {
|
||||||
|
a: {},
|
||||||
|
c: {
|
||||||
|
[`${localRtcMember.sender}:${localRtcMember.deviceId}`]: {
|
||||||
|
reactionEventId,
|
||||||
|
membershipEventId: localRtcMember.eventId,
|
||||||
|
time: localTimestamp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hands rejecting events without a proper membership", () => {
|
||||||
|
const { rtcSession } = getBasicRTCSession([local, alice]);
|
||||||
|
const reactionEventId = "$my_event_id:example.org";
|
||||||
|
const localTimestamp = new Date();
|
||||||
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
|
renderHook(() => {
|
||||||
|
const { raisedHands$ } = useReactionsReader(
|
||||||
|
rtcSession as unknown as MatrixRTCSession,
|
||||||
|
);
|
||||||
|
schedule("ab", {
|
||||||
|
a: () => {},
|
||||||
|
b: () => {
|
||||||
|
rtcSession.room.emit(
|
||||||
|
MatrixRoomEvent.Timeline,
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: rtcSession.room.roomId,
|
||||||
|
event_id: reactionEventId,
|
||||||
|
sender: localRtcMember.sender,
|
||||||
|
type: EventType.Reaction,
|
||||||
|
origin_server_ts: localTimestamp.getTime(),
|
||||||
|
content: {
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: "$not-this-one:example.org",
|
||||||
|
key: "🖐️",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
rtcSession.room,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
{} as IRoomTimelineData,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expectObservable(raisedHands$).toBe("a-", {
|
||||||
|
a: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles a reaction", () => {
|
||||||
|
const { rtcSession } = getBasicRTCSession([local, alice]);
|
||||||
|
const reactionEventId = "$my_event_id:example.org";
|
||||||
|
const reaction = ReactionSet[1];
|
||||||
|
|
||||||
|
vitest.setSystemTime(0);
|
||||||
|
|
||||||
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
|
renderHook(() => {
|
||||||
|
const { reactions$ } = useReactionsReader(
|
||||||
|
rtcSession as unknown as MatrixRTCSession,
|
||||||
|
);
|
||||||
|
schedule("ab", {
|
||||||
|
a: () => {},
|
||||||
|
b: () => {
|
||||||
|
rtcSession.room.emit(
|
||||||
|
MatrixRoomEvent.Timeline,
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: rtcSession.room.roomId,
|
||||||
|
event_id: reactionEventId,
|
||||||
|
sender: localRtcMember.sender,
|
||||||
|
type: ElementCallReactionEventType,
|
||||||
|
content: {
|
||||||
|
emoji: reaction.emoji,
|
||||||
|
name: reaction.name,
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: localRtcMember.eventId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
rtcSession.room,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
{} as IRoomTimelineData,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
c: () => {
|
||||||
|
vitest.advanceTimersByTime(5000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expectObservable(reactions$).toBe(
|
||||||
|
`ab ${REACTION_ACTIVE_TIME_MS - 1}ms c`,
|
||||||
|
{
|
||||||
|
a: {},
|
||||||
|
b: {
|
||||||
|
[`${localRtcMember.sender}:${localRtcMember.deviceId}`]: {
|
||||||
|
reactionOption: reaction,
|
||||||
|
expireAfter: new Date(REACTION_ACTIVE_TIME_MS),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Expect reaction to expire.
|
||||||
|
c: {},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ignores bad reaction events", () => {
|
||||||
|
const { rtcSession } = getBasicRTCSession([local, alice]);
|
||||||
|
const reactionEventId = "$my_event_id:example.org";
|
||||||
|
const reaction = ReactionSet[1];
|
||||||
|
|
||||||
|
vitest.setSystemTime(0);
|
||||||
|
|
||||||
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
|
renderHook(() => {
|
||||||
|
const { reactions$ } = useReactionsReader(
|
||||||
|
rtcSession as unknown as MatrixRTCSession,
|
||||||
|
);
|
||||||
|
schedule("ab", {
|
||||||
|
a: () => {},
|
||||||
|
b: () => {
|
||||||
|
// Missing content
|
||||||
|
rtcSession.room.emit(
|
||||||
|
MatrixRoomEvent.Timeline,
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: rtcSession.room.roomId,
|
||||||
|
event_id: reactionEventId,
|
||||||
|
sender: localRtcMember.sender,
|
||||||
|
type: ElementCallReactionEventType,
|
||||||
|
content: {},
|
||||||
|
}),
|
||||||
|
rtcSession.room,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
{} as IRoomTimelineData,
|
||||||
|
);
|
||||||
|
// Wrong relates event
|
||||||
|
rtcSession.room.emit(
|
||||||
|
MatrixRoomEvent.Timeline,
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: rtcSession.room.roomId,
|
||||||
|
event_id: reactionEventId,
|
||||||
|
sender: localRtcMember.sender,
|
||||||
|
type: ElementCallReactionEventType,
|
||||||
|
content: {
|
||||||
|
emoji: reaction.emoji,
|
||||||
|
name: reaction.name,
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: "wrong-event",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
rtcSession.room,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
{} as IRoomTimelineData,
|
||||||
|
);
|
||||||
|
// Wrong rtc member event
|
||||||
|
rtcSession.room.emit(
|
||||||
|
MatrixRoomEvent.Timeline,
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: rtcSession.room.roomId,
|
||||||
|
event_id: reactionEventId,
|
||||||
|
sender: aliceRtcMember.sender,
|
||||||
|
type: ElementCallReactionEventType,
|
||||||
|
content: {
|
||||||
|
emoji: reaction.emoji,
|
||||||
|
name: reaction.name,
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: localRtcMember.eventId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
rtcSession.room,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
{} as IRoomTimelineData,
|
||||||
|
);
|
||||||
|
// No emoji
|
||||||
|
rtcSession.room.emit(
|
||||||
|
MatrixRoomEvent.Timeline,
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: rtcSession.room.roomId,
|
||||||
|
event_id: reactionEventId,
|
||||||
|
sender: localRtcMember.sender,
|
||||||
|
type: ElementCallReactionEventType,
|
||||||
|
content: {
|
||||||
|
name: reaction.name,
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: localRtcMember.eventId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
rtcSession.room,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
{} as IRoomTimelineData,
|
||||||
|
);
|
||||||
|
// Invalid emoji
|
||||||
|
rtcSession.room.emit(
|
||||||
|
MatrixRoomEvent.Timeline,
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: rtcSession.room.roomId,
|
||||||
|
event_id: reactionEventId,
|
||||||
|
sender: localRtcMember.sender,
|
||||||
|
type: ElementCallReactionEventType,
|
||||||
|
content: {
|
||||||
|
emoji: " ",
|
||||||
|
name: reaction.name,
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: localRtcMember.eventId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
rtcSession.room,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
{} as IRoomTimelineData,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expectObservable(reactions$).toBe("a-", {
|
||||||
|
a: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("that reactions cannot be spammed", () => {
|
||||||
|
const { rtcSession } = getBasicRTCSession([local, alice]);
|
||||||
|
const reactionEventId = "$my_event_id:example.org";
|
||||||
|
const reactionA = ReactionSet[1];
|
||||||
|
const reactionB = ReactionSet[2];
|
||||||
|
|
||||||
|
vitest.setSystemTime(0);
|
||||||
|
|
||||||
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
|
renderHook(() => {
|
||||||
|
const { reactions$ } = useReactionsReader(
|
||||||
|
rtcSession as unknown as MatrixRTCSession,
|
||||||
|
);
|
||||||
|
schedule("abc", {
|
||||||
|
a: () => {},
|
||||||
|
b: () => {
|
||||||
|
rtcSession.room.emit(
|
||||||
|
MatrixRoomEvent.Timeline,
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: rtcSession.room.roomId,
|
||||||
|
event_id: reactionEventId,
|
||||||
|
sender: localRtcMember.sender,
|
||||||
|
type: ElementCallReactionEventType,
|
||||||
|
content: {
|
||||||
|
emoji: reactionA.emoji,
|
||||||
|
name: reactionA.name,
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: localRtcMember.eventId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
rtcSession.room,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
{} as IRoomTimelineData,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
c: () => {
|
||||||
|
rtcSession.room.emit(
|
||||||
|
MatrixRoomEvent.Timeline,
|
||||||
|
new MatrixEvent({
|
||||||
|
room_id: rtcSession.room.roomId,
|
||||||
|
event_id: reactionEventId,
|
||||||
|
sender: localRtcMember.sender,
|
||||||
|
type: ElementCallReactionEventType,
|
||||||
|
content: {
|
||||||
|
emoji: reactionB.emoji,
|
||||||
|
name: reactionB.name,
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: localRtcMember.eventId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
rtcSession.room,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
{} as IRoomTimelineData,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expectObservable(reactions$).toBe(
|
||||||
|
`ab ${REACTION_ACTIVE_TIME_MS - 1}ms c`,
|
||||||
|
{
|
||||||
|
a: {},
|
||||||
|
b: {
|
||||||
|
[`${localRtcMember.sender}:${localRtcMember.deviceId}`]: {
|
||||||
|
reactionOption: reactionA,
|
||||||
|
expireAfter: new Date(REACTION_ACTIVE_TIME_MS),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
c: {},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
EventType,
|
EventType,
|
||||||
RoomEvent as MatrixRoomEvent,
|
RoomEvent as MatrixRoomEvent,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { BehaviorSubject, type Observable } from "rxjs";
|
import { BehaviorSubject, delay, type Observable } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ElementCallReactionEventType,
|
ElementCallReactionEventType,
|
||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
import { useLatest } from "../useLatest";
|
import { useLatest } from "../useLatest";
|
||||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||||
|
|
||||||
const REACTION_ACTIVE_TIME_MS = 3000;
|
export const REACTION_ACTIVE_TIME_MS = 3000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listens for reactions from a RTCSession and populates subjects
|
* Listens for reactions from a RTCSession and populates subjects
|
||||||
@@ -46,6 +46,19 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
|
|||||||
new BehaviorSubject<Record<string, ReactionInfo>>({}),
|
new BehaviorSubject<Record<string, ReactionInfo>>({}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
reactionsSubject$.current
|
||||||
|
.pipe(delay(REACTION_ACTIVE_TIME_MS))
|
||||||
|
.subscribe((reactions) => {
|
||||||
|
const date = new Date();
|
||||||
|
const nextEntries = Object.fromEntries(
|
||||||
|
Object.entries(reactions).filter(([_, hr]) => hr.expireAfter < date),
|
||||||
|
);
|
||||||
|
if (Object.keys(reactions).length === Object.keys(nextEntries).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reactionsSubject$.current.next(nextEntries);
|
||||||
|
});
|
||||||
|
|
||||||
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||||
const latestMemberships = useLatest(memberships);
|
const latestMemberships = useLatest(memberships);
|
||||||
const latestRaisedHands = useLatest(raisedHandsSubject$.current);
|
const latestRaisedHands = useLatest(raisedHandsSubject$.current);
|
||||||
@@ -132,7 +145,6 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
|
|||||||
|
|
||||||
// This effect handles any *live* reaction/redactions in the room.
|
// This effect handles any *live* reaction/redactions in the room.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const reactionTimeouts = new Set<NodeJS.Timeout>();
|
|
||||||
const handleReactionEvent = (event: MatrixEvent): void => {
|
const handleReactionEvent = (event: MatrixEvent): void => {
|
||||||
// Decrypted events might come from a different room
|
// Decrypted events might come from a different room
|
||||||
if (event.getRoomId() !== room.roomId) return;
|
if (event.getRoomId() !== room.roomId) return;
|
||||||
@@ -178,7 +190,7 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
|
|||||||
[Symbol.iterator]();
|
[Symbol.iterator]();
|
||||||
const emoji = segment.next().value?.segment;
|
const emoji = segment.next().value?.segment;
|
||||||
|
|
||||||
if (!emoji) {
|
if (!emoji?.trim()) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Reaction had no emoji from ${reactionEventId} after splitting`,
|
`Reaction had no emoji from ${reactionEventId} after splitting`,
|
||||||
);
|
);
|
||||||
@@ -198,23 +210,11 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
|
|||||||
// 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;
|
return;
|
||||||
}
|
}
|
||||||
const timeout = globalThis.setTimeout(() => {
|
|
||||||
// Clear the reaction after some time.
|
|
||||||
reactionsSubject$.current.next(
|
|
||||||
Object.fromEntries(
|
|
||||||
Object.entries(reactionsSubject$.current.value).filter(
|
|
||||||
([id]) => id !== identifier,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
reactionTimeouts.delete(timeout);
|
|
||||||
}, REACTION_ACTIVE_TIME_MS);
|
|
||||||
reactionTimeouts.add(timeout);
|
|
||||||
reactionsSubject$.current.next({
|
reactionsSubject$.current.next({
|
||||||
...currentReactions,
|
...currentReactions,
|
||||||
[identifier]: {
|
[identifier]: {
|
||||||
reactionOption: reaction,
|
reactionOption: reaction,
|
||||||
ttl: 0,
|
expireAfter: new Date(Date.now() + REACTION_ACTIVE_TIME_MS),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (event.getType() === EventType.Reaction) {
|
} else if (event.getType() === EventType.Reaction) {
|
||||||
@@ -264,15 +264,11 @@ export default function useReactionsReader(rtcSession: MatrixRTCSession): {
|
|||||||
// may still be sending.
|
// may still be sending.
|
||||||
room.on(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent);
|
room.on(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent);
|
||||||
|
|
||||||
const innerReactionsSubject$ = reactionsSubject$.current;
|
|
||||||
return (): void => {
|
return (): void => {
|
||||||
room.off(MatrixRoomEvent.Timeline, handleReactionEvent);
|
room.off(MatrixRoomEvent.Timeline, handleReactionEvent);
|
||||||
room.off(MatrixRoomEvent.Redaction, handleReactionEvent);
|
room.off(MatrixRoomEvent.Redaction, handleReactionEvent);
|
||||||
room.client.off(MatrixEventEvent.Decrypted, handleReactionEvent);
|
room.client.off(MatrixEventEvent.Decrypted, handleReactionEvent);
|
||||||
room.off(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent);
|
room.off(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent);
|
||||||
reactionTimeouts.forEach((t) => clearTimeout(t));
|
|
||||||
// If we're clearing timeouts, we also clear all reactions.
|
|
||||||
innerReactionsSubject$.next({});
|
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
room,
|
room,
|
||||||
|
|||||||
@@ -96,7 +96,10 @@ test("will play an audio sound when there is a reaction", () => {
|
|||||||
}
|
}
|
||||||
act(() => {
|
act(() => {
|
||||||
reactionsSubject$.next({
|
reactionsSubject$.next({
|
||||||
[aliceRtcMember.deviceId]: { reactionOption: chosenReaction, ttl: 0 },
|
[aliceRtcMember.deviceId]: {
|
||||||
|
reactionOption: chosenReaction,
|
||||||
|
expireAfter: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(playSound).toHaveBeenCalledWith(chosenReaction.name);
|
expect(playSound).toHaveBeenCalledWith(chosenReaction.name);
|
||||||
@@ -119,7 +122,10 @@ test("will play the generic audio sound when there is soundless reaction", () =>
|
|||||||
}
|
}
|
||||||
act(() => {
|
act(() => {
|
||||||
reactionsSubject$.next({
|
reactionsSubject$.next({
|
||||||
[aliceRtcMember.deviceId]: { reactionOption: chosenReaction, ttl: 0 },
|
[aliceRtcMember.deviceId]: {
|
||||||
|
reactionOption: chosenReaction,
|
||||||
|
expireAfter: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(playSound).toHaveBeenCalledWith(GenericReaction.name);
|
expect(playSound).toHaveBeenCalledWith(GenericReaction.name);
|
||||||
@@ -142,9 +148,9 @@ test("will play multiple audio sounds when there are multiple different reaction
|
|||||||
}
|
}
|
||||||
act(() => {
|
act(() => {
|
||||||
reactionsSubject$.next({
|
reactionsSubject$.next({
|
||||||
[aliceRtcMember.deviceId]: { reactionOption: reaction1, ttl: 0 },
|
[aliceRtcMember.deviceId]: { reactionOption: reaction1, expireAfter: 0 },
|
||||||
[bobRtcMember.deviceId]: { reactionOption: reaction2, ttl: 0 },
|
[bobRtcMember.deviceId]: { reactionOption: reaction2, expireAfter: 0 },
|
||||||
[localRtcMember.deviceId]: { reactionOption: reaction1, ttl: 0 },
|
[localRtcMember.deviceId]: { reactionOption: reaction1, expireAfter: 0 },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(playSound).toHaveBeenCalledWith(reaction1.name);
|
expect(playSound).toHaveBeenCalledWith(reaction1.name);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ test("shows a reaction when sent", () => {
|
|||||||
const reaction = ReactionSet[0];
|
const reaction = ReactionSet[0];
|
||||||
act(() => {
|
act(() => {
|
||||||
reactionsSubject$.next({
|
reactionsSubject$.next({
|
||||||
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
|
[aliceRtcMember.deviceId]: { reactionOption: reaction, expireAfter: 0 },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const span = getByRole("presentation");
|
const span = getByRole("presentation");
|
||||||
@@ -60,8 +60,8 @@ test("shows two of the same reaction when sent", () => {
|
|||||||
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
|
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
|
||||||
act(() => {
|
act(() => {
|
||||||
reactionsSubject$.next({
|
reactionsSubject$.next({
|
||||||
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
|
[aliceRtcMember.deviceId]: { reactionOption: reaction, expireAfter: 0 },
|
||||||
[bobRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
|
[bobRtcMember.deviceId]: { reactionOption: reaction, expireAfter: 0 },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(getAllByRole("presentation")).toHaveLength(2);
|
expect(getAllByRole("presentation")).toHaveLength(2);
|
||||||
@@ -77,8 +77,8 @@ test("shows two different reactions when sent", () => {
|
|||||||
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
|
const { getAllByRole } = render(<ReactionsOverlay vm={vm} />);
|
||||||
act(() => {
|
act(() => {
|
||||||
reactionsSubject$.next({
|
reactionsSubject$.next({
|
||||||
[aliceRtcMember.deviceId]: { reactionOption: reactionA, ttl: 0 },
|
[aliceRtcMember.deviceId]: { reactionOption: reactionA, expireAfter: 0 },
|
||||||
[bobRtcMember.deviceId]: { reactionOption: reactionB, ttl: 0 },
|
[bobRtcMember.deviceId]: { reactionOption: reactionB, expireAfter: 0 },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const [reactionElementA, reactionElementB] = getAllByRole("presentation");
|
const [reactionElementA, reactionElementB] = getAllByRole("presentation");
|
||||||
@@ -96,7 +96,7 @@ test("hides reactions when reaction animations are disabled", () => {
|
|||||||
const { container } = render(<ReactionsOverlay vm={vm} />);
|
const { container } = render(<ReactionsOverlay vm={vm} />);
|
||||||
act(() => {
|
act(() => {
|
||||||
reactionsSubject$.next({
|
reactionsSubject$.next({
|
||||||
[aliceRtcMember.deviceId]: { reactionOption: reaction, ttl: 0 },
|
[aliceRtcMember.deviceId]: { reactionOption: reaction, expireAfter: 0 },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(container.getElementsByTagName("span")).toHaveLength(0);
|
expect(container.getElementsByTagName("span")).toHaveLength(0);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { BehaviorSubject, of } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
import { vitest } from "vitest";
|
import { vitest } from "vitest";
|
||||||
import { type RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
import { type RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
||||||
|
import EventEmitter from "events";
|
||||||
|
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
import { CallViewModel } from "../state/CallViewModel";
|
import { CallViewModel } from "../state/CallViewModel";
|
||||||
@@ -27,6 +28,80 @@ import {
|
|||||||
} from "./test-fixtures";
|
} from "./test-fixtures";
|
||||||
import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
|
import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
|
||||||
|
|
||||||
|
export function getBasicRTCSession(
|
||||||
|
members: RoomMember[],
|
||||||
|
initialRemoteRtcMemberships: CallMembership[] = [aliceRtcMember],
|
||||||
|
): {
|
||||||
|
rtcSession: MockRTCSession;
|
||||||
|
remoteRtcMemberships$: BehaviorSubject<CallMembership[]>;
|
||||||
|
} {
|
||||||
|
const matrixRoomId = "!myRoomId:example.com";
|
||||||
|
const matrixRoomMembers = new Map(members.map((p) => [p.userId, p]));
|
||||||
|
|
||||||
|
const roomEmitter = new EventEmitter();
|
||||||
|
const clientEmitter = new EventEmitter();
|
||||||
|
const matrixRoom = mockMatrixRoom({
|
||||||
|
relations: {
|
||||||
|
getChildEventsForEvent: vitest.fn(),
|
||||||
|
} as Partial<RelationsContainer> as RelationsContainer,
|
||||||
|
client: {
|
||||||
|
getUserId: () => localRtcMember.sender,
|
||||||
|
getDeviceId: () => localRtcMember.deviceId,
|
||||||
|
sendEvent: vitest.fn().mockResolvedValue({ event_id: "$fake:event" }),
|
||||||
|
redactEvent: vitest.fn().mockResolvedValue({ event_id: "$fake:event" }),
|
||||||
|
decryptEventIfNeeded: vitest.fn().mockResolvedValue(undefined),
|
||||||
|
on: vitest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(
|
||||||
|
(eventName: string, fn: (...args: unknown[]) => void) => {
|
||||||
|
clientEmitter.on(eventName, fn);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
emit: (eventName: string, ...args: unknown[]) =>
|
||||||
|
clientEmitter.emit(eventName, ...args),
|
||||||
|
off: vitest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(
|
||||||
|
(eventName: string, fn: (...args: unknown[]) => void) => {
|
||||||
|
clientEmitter.off(eventName, fn);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
} as Partial<MatrixClient> as MatrixClient,
|
||||||
|
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
|
||||||
|
roomId: matrixRoomId,
|
||||||
|
on: vitest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(
|
||||||
|
(eventName: string, fn: (...args: unknown[]) => void) => {
|
||||||
|
roomEmitter.on(eventName, fn);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
emit: (eventName: string, ...args: unknown[]) =>
|
||||||
|
roomEmitter.emit(eventName, ...args),
|
||||||
|
off: vitest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(
|
||||||
|
(eventName: string, fn: (...args: unknown[]) => void) => {
|
||||||
|
roomEmitter.off(eventName, fn);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const remoteRtcMemberships$ = new BehaviorSubject<CallMembership[]>(
|
||||||
|
initialRemoteRtcMemberships,
|
||||||
|
);
|
||||||
|
|
||||||
|
const rtcSession = new MockRTCSession(
|
||||||
|
matrixRoom,
|
||||||
|
localRtcMember,
|
||||||
|
).withMemberships(remoteRtcMemberships$);
|
||||||
|
|
||||||
|
return {
|
||||||
|
rtcSession,
|
||||||
|
remoteRtcMemberships$,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a basic CallViewModel to test components that make use of it.
|
* Construct a basic CallViewModel to test components that make use of it.
|
||||||
* @param members
|
* @param members
|
||||||
@@ -43,41 +118,18 @@ export function getBasicCallViewModelEnvironment(
|
|||||||
handRaisedSubject$: BehaviorSubject<Record<string, RaisedHandInfo>>;
|
handRaisedSubject$: BehaviorSubject<Record<string, RaisedHandInfo>>;
|
||||||
reactionsSubject$: BehaviorSubject<Record<string, ReactionInfo>>;
|
reactionsSubject$: BehaviorSubject<Record<string, ReactionInfo>>;
|
||||||
} {
|
} {
|
||||||
const matrixRoomId = "!myRoomId:example.com";
|
const { rtcSession, remoteRtcMemberships$ } = getBasicRTCSession(
|
||||||
const matrixRoomMembers = new Map(members.map((p) => [p.userId, p]));
|
members,
|
||||||
|
initialRemoteRtcMemberships,
|
||||||
|
);
|
||||||
|
const handRaisedSubject$ = new BehaviorSubject({});
|
||||||
|
const reactionsSubject$ = new BehaviorSubject({});
|
||||||
|
|
||||||
const remoteParticipants$ = of([aliceParticipant]);
|
const remoteParticipants$ = of([aliceParticipant]);
|
||||||
const liveKitRoom = mockLivekitRoom(
|
const liveKitRoom = mockLivekitRoom(
|
||||||
{ localParticipant },
|
{ localParticipant },
|
||||||
{ remoteParticipants$ },
|
{ remoteParticipants$ },
|
||||||
);
|
);
|
||||||
const matrixRoom = mockMatrixRoom({
|
|
||||||
relations: {
|
|
||||||
getChildEventsForEvent: vitest.fn(),
|
|
||||||
} as Partial<RelationsContainer> as RelationsContainer,
|
|
||||||
client: {
|
|
||||||
getUserId: () => localRtcMember.sender,
|
|
||||||
getDeviceId: () => localRtcMember.deviceId,
|
|
||||||
sendEvent: vitest.fn().mockResolvedValue({ event_id: "$fake:event" }),
|
|
||||||
redactEvent: vitest.fn().mockResolvedValue({ event_id: "$fake:event" }),
|
|
||||||
on: vitest.fn(),
|
|
||||||
off: vitest.fn(),
|
|
||||||
} as Partial<MatrixClient> as MatrixClient,
|
|
||||||
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
|
|
||||||
roomId: matrixRoomId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const remoteRtcMemberships$ = new BehaviorSubject<CallMembership[]>(
|
|
||||||
initialRemoteRtcMemberships,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handRaisedSubject$ = new BehaviorSubject({});
|
|
||||||
const reactionsSubject$ = new BehaviorSubject({});
|
|
||||||
|
|
||||||
const rtcSession = new MockRTCSession(
|
|
||||||
matrixRoom,
|
|
||||||
localRtcMember,
|
|
||||||
).withMemberships(remoteRtcMemberships$);
|
|
||||||
|
|
||||||
const vm = new CallViewModel(
|
const vm = new CallViewModel(
|
||||||
rtcSession as unknown as MatrixRTCSession,
|
rtcSession as unknown as MatrixRTCSession,
|
||||||
liveKitRoom,
|
liveKitRoom,
|
||||||
@@ -90,7 +142,7 @@ export function getBasicCallViewModelEnvironment(
|
|||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
vm,
|
vm,
|
||||||
remoteRtcMemberships$: remoteRtcMemberships$,
|
remoteRtcMemberships$,
|
||||||
rtcSession,
|
rtcSession,
|
||||||
handRaisedSubject$: handRaisedSubject$,
|
handRaisedSubject$: handRaisedSubject$,
|
||||||
reactionsSubject$: reactionsSubject$,
|
reactionsSubject$: reactionsSubject$,
|
||||||
|
|||||||
Reference in New Issue
Block a user