- {encryptionStatus !== EncryptionStatus.Okay && (
+
+
+ {currentReaction && (
+
+ )}
+
+ {/* TODO: Bring this back once encryption status is less broken */}
+ {/*encryptionStatus !== EncryptionStatus.Okay && (
{encryptionStatus === EncryptionStatus.Connecting &&
@@ -111,13 +130,7 @@ export const MediaView = forwardRef(
t("e2ee_encryption_status.password_invalid")}
- )}
-
+ )*/}
{nameTagLeadingIcon}
diff --git a/src/useMediaQuery.ts b/src/useMediaQuery.ts
index b98eb349..14d8cf03 100644
--- a/src/useMediaQuery.ts
+++ b/src/useMediaQuery.ts
@@ -13,7 +13,7 @@ import { useEventTarget } from "./useEvents";
* React hook that tracks whether the given media query matches.
*/
export function useMediaQuery(query: string): boolean {
- const mediaQuery = useMemo(() => matchMedia(query), [query]);
+ const mediaQuery = useMemo(() => window.matchMedia(query), [query]);
const [numChanges, setNumChanges] = useState(0);
useEventTarget(
diff --git a/src/useReactions.test.tsx b/src/useReactions.test.tsx
index cf017b1e..6140793f 100644
--- a/src/useReactions.test.tsx
+++ b/src/useReactions.test.tsx
@@ -5,34 +5,19 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
-import { act, render } from "@testing-library/react";
-import { FC, ReactNode } from "react";
+import { render } from "@testing-library/react";
+import { act, FC } from "react";
import { describe, expect, test } from "vitest";
-import {
- MatrixRTCSession,
- MatrixRTCSessionEvent,
-} from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
-import {
- EventTimeline,
- EventTimelineSet,
- EventType,
- MatrixClient,
- MatrixEvent,
- Room,
- RoomEvent,
-} from "matrix-js-sdk/src/matrix";
-import EventEmitter from "events";
-import { randomUUID } from "crypto";
-import { CallMembership } from "matrix-js-sdk/src/matrixrtc";
+import { RoomEvent } from "matrix-js-sdk/src/matrix";
-import { ReactionsProvider, useReactions } from "./useReactions";
-
-/**
- * Test explanation.
- * This test suite checks that the useReactions hook appropriately reacts
- * to new reactions, redactions and membership changesin the room. There is
- * a large amount of test structure used to construct a mock environment.
- */
+import { useReactions } from "./useReactions";
+import {
+ createHandRaisedReaction,
+ createRedaction,
+ MockRoom,
+ MockRTCSession,
+ TestReactionsWrapper,
+} from "./utils/testReactions";
const memberUserIdAlice = "@alice:example.org";
const memberEventAlice = "$membership-alice:example.org";
@@ -45,6 +30,13 @@ const membership: Record = {
"$membership-charlie:example.org": "@charlie:example.org",
};
+/**
+ * Test explanation.
+ * This test suite checks that the useReactions hook appropriately reacts
+ * to new reactions, redactions and membership changesin the room. There is
+ * a large amount of test structure used to construct a mock environment.
+ */
+
const TestComponent: FC = () => {
const { raisedHands } = useReactions();
return (
@@ -61,138 +53,42 @@ const TestComponent: FC = () => {
);
};
-const TestComponentWrapper = ({
- rtcSession,
-}: {
- rtcSession: MockRTCSession;
-}): ReactNode => {
- return (
-
-
-
- );
-};
-
-export class MockRTCSession extends EventEmitter {
- public memberships: Partial[] = Object.entries(
- membership,
- ).map(([eventId, sender]) => ({
- sender,
- eventId,
- createdTs: (): number => Date.now(),
- }));
-
- public constructor(public readonly room: MockRoom) {
- super();
- }
-
- public testRemoveMember(userId: string): void {
- this.memberships = this.memberships.filter((u) => u.sender !== userId);
- this.emit(MatrixRTCSessionEvent.MembershipsChanged);
- }
-
- public testAddMember(sender: string): void {
- this.memberships.push({
- sender,
- eventId: `!fake-${randomUUID()}:event`,
- createdTs: (): number => Date.now(),
- });
- this.emit(MatrixRTCSessionEvent.MembershipsChanged);
- }
-}
-
-function createReaction(
- parentMemberEvent: string,
- overridenSender?: string,
-): MatrixEvent {
- return new MatrixEvent({
- sender: overridenSender ?? membership[parentMemberEvent],
- type: EventType.Reaction,
- origin_server_ts: new Date().getTime(),
- content: {
- "m.relates_to": {
- key: "🖐️",
- event_id: parentMemberEvent,
- },
- },
- event_id: randomUUID(),
- });
-}
-
-function createRedaction(sender: string, reactionEventId: string): MatrixEvent {
- return new MatrixEvent({
- sender,
- type: EventType.RoomRedaction,
- origin_server_ts: new Date().getTime(),
- redacts: reactionEventId,
- content: {},
- event_id: randomUUID(),
- });
-}
-
-export class MockRoom extends EventEmitter {
- public constructor(private readonly existingRelations: MatrixEvent[] = []) {
- super();
- }
-
- public get client(): MatrixClient {
- return {
- getUserId: (): string => memberUserIdAlice,
- } as unknown as MatrixClient;
- }
-
- public get relations(): Room["relations"] {
- return {
- getChildEventsForEvent: (membershipEventId: string) => ({
- getRelations: (): MatrixEvent[] => {
- return this.existingRelations.filter(
- (r) =>
- r.getContent()["m.relates_to"]?.event_id === membershipEventId,
- );
- },
- }),
- } as unknown as Room["relations"];
- }
-
- public testSendReaction(
- parentMemberEvent: string,
- overridenSender?: string,
- ): string {
- const evt = createReaction(parentMemberEvent, overridenSender);
- this.emit(RoomEvent.Timeline, evt, this, undefined, false, {
- timeline: new EventTimeline(new EventTimelineSet(undefined)),
- });
- return evt.getId()!;
- }
-}
-
describe("useReactions", () => {
test("starts with an empty list", () => {
- const rtcSession = new MockRTCSession(new MockRoom());
+ const rtcSession = new MockRTCSession(
+ new MockRoom(memberUserIdAlice),
+ membership,
+ );
const { queryByRole } = render(
- ,
+
+
+ ,
);
expect(queryByRole("list")?.children).to.have.lengthOf(0);
});
test("handles incoming raised hand", async () => {
- const room = new MockRoom();
- const rtcSession = new MockRTCSession(room);
+ const room = new MockRoom(memberUserIdAlice);
+ const rtcSession = new MockRTCSession(room, membership);
const { queryByRole } = render(
- ,
+
+
+ ,
);
- await act(() => room.testSendReaction(memberEventAlice));
+ await act(() => room.testSendHandRaise(memberEventAlice, membership));
expect(queryByRole("list")?.children).to.have.lengthOf(1);
- await act(() => room.testSendReaction(memberEventBob));
+ await act(() => room.testSendHandRaise(memberEventBob, membership));
expect(queryByRole("list")?.children).to.have.lengthOf(2);
});
test("handles incoming unraised hand", async () => {
- const room = new MockRoom();
- const rtcSession = new MockRTCSession(room);
+ const room = new MockRoom(memberUserIdAlice);
+ const rtcSession = new MockRTCSession(room, membership);
const { queryByRole } = render(
- ,
+
+
+ ,
);
const reactionEventId = await act(() =>
- room.testSendReaction(memberEventAlice),
+ room.testSendHandRaise(memberEventAlice, membership),
);
expect(queryByRole("list")?.children).to.have.lengthOf(1);
await act(() =>
@@ -206,30 +102,42 @@ describe("useReactions", () => {
expect(queryByRole("list")?.children).to.have.lengthOf(0);
});
test("handles loading prior raised hand events", () => {
- const room = new MockRoom([createReaction(memberEventAlice)]);
- const rtcSession = new MockRTCSession(room);
+ const room = new MockRoom(memberUserIdAlice, [
+ createHandRaisedReaction(memberEventAlice, membership),
+ ]);
+ const rtcSession = new MockRTCSession(room, membership);
const { queryByRole } = render(
- ,
+
+
+ ,
);
expect(queryByRole("list")?.children).to.have.lengthOf(1);
});
// If the membership event changes for a user, we want to remove
// the raised hand event.
test("will remove reaction when a member leaves the call", () => {
- const room = new MockRoom([createReaction(memberEventAlice)]);
- const rtcSession = new MockRTCSession(room);
+ const room = new MockRoom(memberUserIdAlice, [
+ createHandRaisedReaction(memberEventAlice, membership),
+ ]);
+ const rtcSession = new MockRTCSession(room, membership);
const { queryByRole } = render(
- ,
+
+
+ ,
);
expect(queryByRole("list")?.children).to.have.lengthOf(1);
act(() => rtcSession.testRemoveMember(memberUserIdAlice));
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(memberEventAlice)]);
- const rtcSession = new MockRTCSession(room);
+ const room = new MockRoom(memberUserIdAlice, [
+ createHandRaisedReaction(memberEventAlice, membership),
+ ]);
+ const rtcSession = new MockRTCSession(room, membership);
const { queryByRole } = render(
- ,
+
+
+ ,
);
expect(queryByRole("list")?.children).to.have.lengthOf(1);
// Simulate leaving and rejoining
@@ -240,22 +148,26 @@ describe("useReactions", () => {
expect(queryByRole("list")?.children).to.have.lengthOf(0);
});
test("ignores invalid sender for historic event", () => {
- const room = new MockRoom([
- createReaction(memberEventAlice, memberUserIdBob),
+ const room = new MockRoom(memberUserIdAlice, [
+ createHandRaisedReaction(memberEventAlice, memberUserIdBob),
]);
- const rtcSession = new MockRTCSession(room);
+ const rtcSession = new MockRTCSession(room, membership);
const { queryByRole } = render(
- ,
+
+
+ ,
);
expect(queryByRole("list")?.children).to.have.lengthOf(0);
});
test("ignores invalid sender for new event", async () => {
- const room = new MockRoom([]);
- const rtcSession = new MockRTCSession(room);
+ const room = new MockRoom(memberUserIdAlice);
+ const rtcSession = new MockRTCSession(room, membership);
const { queryByRole } = render(
- ,
+
+
+ ,
);
- await act(() => room.testSendReaction(memberEventAlice, memberUserIdBob));
+ await act(() => room.testSendHandRaise(memberEventAlice, memberUserIdBob));
expect(queryByRole("list")?.children).to.have.lengthOf(0);
});
});
diff --git a/src/useReactions.tsx b/src/useReactions.tsx
index 7ce478b5..c8d3c3b5 100644
--- a/src/useReactions.tsx
+++ b/src/useReactions.tsx
@@ -10,6 +10,7 @@ import {
MatrixEvent,
RelationType,
RoomEvent as MatrixRoomEvent,
+ MatrixEventEvent,
} from "matrix-js-sdk/src/matrix";
import { ReactionEventContent } from "matrix-js-sdk/src/types";
import {
@@ -26,10 +27,19 @@ import { logger } from "matrix-js-sdk/src/logger";
import { useMatrixRTCSessionMemberships } from "./useMatrixRTCSessionMemberships";
import { useClientState } from "./ClientContext";
+import {
+ ECallReactionEventContent,
+ ElementCallReactionEventType,
+ GenericReaction,
+ ReactionOption,
+ ReactionSet,
+} from "./reactions";
+import { useLatest } from "./useLatest";
interface ReactionsContextType {
raisedHands: Record;
supportsReactions: boolean;
+ reactions: Record;
lowerHand: () => Promise;
}
@@ -52,6 +62,8 @@ interface RaisedHandInfo {
time: Date;
}
+const REACTION_ACTIVE_TIME_MS = 3000;
+
export const useReactions = (): ReactionsContextType => {
const context = useContext(ReactionsContext);
if (!context) {
@@ -80,6 +92,10 @@ export const ReactionsProvider = ({
const room = rtcSession.room;
const myUserId = room.client.getUserId();
+ const [reactions, setReactions] = useState>(
+ {},
+ );
+
// Reduce the data down for the consumers.
const resultRaisedHands = useMemo(
() =>
@@ -162,29 +178,95 @@ export const ReactionsProvider = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [room, memberships, myUserId, addRaisedHand, removeRaisedHand]);
+ const latestMemberships = useLatest(memberships);
+ const latestRaisedHands = useLatest(raisedHands);
+
// This effect handles any *live* reaction/redactions in the room.
useEffect(() => {
+ const reactionTimeouts = new Set();
const handleReactionEvent = (event: MatrixEvent): void => {
- if (event.isSending()) {
- // Skip any events that are still sending.
- return;
- }
+ // Decrypted events might come from a different room
+ if (event.getRoomId() !== room.roomId) return;
+ // Skip any events that are still sending.
+ if (event.isSending()) return;
const sender = event.getSender();
const reactionEventId = event.getId();
- if (!sender || !reactionEventId) {
- // Skip any event without a sender or event ID.
- return;
- }
+ // Skip any event without a sender or event ID.
+ if (!sender || !reactionEventId) return;
- if (event.getType() === EventType.Reaction) {
+ if (event.getType() === ElementCallReactionEventType) {
+ room.client
+ .decryptEventIfNeeded(event)
+ .catch((e) => logger.warn(`Failed to decrypt ${event.getId()}`, e));
+ if (event.isBeingDecrypted() || event.isDecryptionFailure()) return;
+ const content: ECallReactionEventContent = event.getContent();
+
+ const membershipEventId = content?.["m.relates_to"]?.event_id;
+ // Check to see if this reaction was made to a membership event (and the
+ // sender of the reaction matches the membership)
+ if (
+ !latestMemberships.current.some(
+ (e) => e.eventId === membershipEventId && e.sender === sender,
+ )
+ ) {
+ logger.warn(
+ `Reaction target was not a membership event for ${sender}, ignoring`,
+ );
+ return;
+ }
+
+ if (!content.emoji) {
+ logger.warn(`Reaction had no emoji from ${reactionEventId}`);
+ return;
+ }
+
+ const segment = new Intl.Segmenter(undefined, {
+ granularity: "grapheme",
+ })
+ .segment(content.emoji)
+ [Symbol.iterator]();
+ const emoji = segment.next().value?.segment;
+
+ if (!emoji) {
+ logger.warn(
+ `Reaction had no emoji from ${reactionEventId} after splitting`,
+ );
+ return;
+ }
+
+ // One of our custom reactions
+ const reaction = {
+ ...GenericReaction,
+ emoji,
+ // If we don't find a reaction, we can fallback to the generic sound.
+ ...ReactionSet.find((r) => r.name === content.name),
+ };
+
+ setReactions((reactions) => {
+ if (reactions[sender]) {
+ // We've still got a reaction from this user, ignore it to prevent spamming
+ return reactions;
+ }
+ const timeout = window.setTimeout(() => {
+ // Clear the reaction after some time.
+ setReactions(({ [sender]: _unused, ...remaining }) => remaining);
+ reactionTimeouts.delete(timeout);
+ }, REACTION_ACTIVE_TIME_MS);
+ reactionTimeouts.add(timeout);
+ return {
+ ...reactions,
+ [sender]: reaction,
+ };
+ });
+ } else if (event.getType() === EventType.Reaction) {
const content = event.getContent() as ReactionEventContent;
const membershipEventId = content["m.relates_to"].event_id;
// Check to see if this reaction was made to a membership event (and the
// sender of the reaction matches the membership)
if (
- !memberships.some(
+ !latestMemberships.current.some(
(e) => e.eventId === membershipEventId && e.sender === sender,
)
) {
@@ -203,7 +285,7 @@ export const ReactionsProvider = ({
}
} else if (event.getType() === EventType.RoomRedaction) {
const targetEvent = event.event.redacts;
- const targetUser = Object.entries(raisedHands).find(
+ const targetUser = Object.entries(latestRaisedHands.current).find(
([_u, r]) => r.reactionEventId === targetEvent,
)?.[0];
if (!targetUser) {
@@ -216,6 +298,7 @@ export const ReactionsProvider = ({
room.on(MatrixRoomEvent.Timeline, handleReactionEvent);
room.on(MatrixRoomEvent.Redaction, handleReactionEvent);
+ room.client.on(MatrixEventEvent.Decrypted, handleReactionEvent);
// We listen for a local echo to get the real event ID, as timeline events
// may still be sending.
@@ -224,17 +307,22 @@ export const ReactionsProvider = ({
return (): void => {
room.off(MatrixRoomEvent.Timeline, handleReactionEvent);
room.off(MatrixRoomEvent.Redaction, handleReactionEvent);
+ room.client.off(MatrixEventEvent.Decrypted, handleReactionEvent);
room.off(MatrixRoomEvent.LocalEchoUpdated, handleReactionEvent);
+ reactionTimeouts.forEach((t) => clearTimeout(t));
+ // If we're clearing timeouts, we also clear all reactions.
+ setReactions({});
};
- }, [room, addRaisedHand, removeRaisedHand, memberships, raisedHands]);
+ }, [
+ room,
+ addRaisedHand,
+ removeRaisedHand,
+ latestMemberships,
+ latestRaisedHands,
+ ]);
const lowerHand = useCallback(async () => {
- if (
- !myUserId ||
- clientState?.state !== "valid" ||
- !clientState.authenticated ||
- !raisedHands[myUserId]
- ) {
+ if (!myUserId || !raisedHands[myUserId]) {
return;
}
const myReactionId = raisedHands[myUserId].reactionEventId;
@@ -243,21 +331,19 @@ export const ReactionsProvider = ({
return;
}
try {
- await clientState.authenticated.client.redactEvent(
- rtcSession.room.roomId,
- myReactionId,
- );
+ await room.client.redactEvent(rtcSession.room.roomId, myReactionId);
logger.debug("Redacted raise hand event");
} catch (ex) {
logger.error("Failed to redact reaction event", myReactionId, ex);
}
- }, [myUserId, raisedHands, clientState, rtcSession]);
+ }, [myUserId, raisedHands, rtcSession, room]);
return (
diff --git a/src/utils/testReactions.tsx b/src/utils/testReactions.tsx
new file mode 100644
index 00000000..84ff217b
--- /dev/null
+++ b/src/utils/testReactions.tsx
@@ -0,0 +1,206 @@
+/*
+Copyright 2024 New Vector Ltd.
+
+SPDX-License-Identifier: AGPL-3.0-only
+Please see LICENSE in the repository root for full details.
+*/
+
+import { PropsWithChildren, ReactNode } from "react";
+import { randomUUID } from "crypto";
+import EventEmitter from "events";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { EventType, RoomEvent, RelationType } from "matrix-js-sdk/src/matrix";
+import {
+ MatrixEvent,
+ EventTimeline,
+ EventTimelineSet,
+ Room,
+} from "matrix-js-sdk/src/matrix";
+import {
+ MatrixRTCSession,
+ MatrixRTCSessionEvent,
+} from "matrix-js-sdk/src/matrixrtc";
+
+import { ReactionsProvider } from "../useReactions";
+import {
+ ECallReactionEventContent,
+ ElementCallReactionEventType,
+ ReactionOption,
+} from "../reactions";
+
+export const TestReactionsWrapper = ({
+ rtcSession,
+ children,
+}: PropsWithChildren<{
+ rtcSession: MockRTCSession;
+}>): ReactNode => {
+ return (
+
+ {children}
+
+ );
+};
+
+export class MockRTCSession extends EventEmitter {
+ public memberships: {
+ sender: string;
+ eventId: string;
+ createdTs: () => Date;
+ }[];
+
+ public constructor(
+ public readonly room: MockRoom,
+ membership: Record,
+ ) {
+ super();
+ this.memberships = Object.entries(membership).map(([eventId, sender]) => ({
+ sender,
+ eventId,
+ createdTs: (): Date => new Date(),
+ }));
+ }
+
+ public testRemoveMember(userId: string): void {
+ this.memberships = this.memberships.filter((u) => u.sender !== userId);
+ this.emit(MatrixRTCSessionEvent.MembershipsChanged);
+ }
+
+ public testAddMember(sender: string): void {
+ this.memberships.push({
+ sender,
+ eventId: `!fake-${randomUUID()}:event`,
+ createdTs: (): Date => new Date(),
+ });
+ this.emit(MatrixRTCSessionEvent.MembershipsChanged);
+ }
+}
+
+export function createHandRaisedReaction(
+ parentMemberEvent: string,
+ membershipOrOverridenSender: Record | string,
+): MatrixEvent {
+ return new MatrixEvent({
+ sender:
+ typeof membershipOrOverridenSender === "string"
+ ? membershipOrOverridenSender
+ : membershipOrOverridenSender[parentMemberEvent],
+ type: EventType.Reaction,
+ origin_server_ts: new Date().getTime(),
+ content: {
+ "m.relates_to": {
+ key: "🖐️",
+ event_id: parentMemberEvent,
+ },
+ },
+ event_id: randomUUID(),
+ });
+}
+
+export function createRedaction(
+ sender: string,
+ reactionEventId: string,
+): MatrixEvent {
+ return new MatrixEvent({
+ sender,
+ type: EventType.RoomRedaction,
+ origin_server_ts: new Date().getTime(),
+ redacts: reactionEventId,
+ content: {},
+ event_id: randomUUID(),
+ });
+}
+
+export class MockRoom extends EventEmitter {
+ public readonly testSentEvents: Parameters[] = [];
+ public readonly testRedactedEvents: Parameters<
+ MatrixClient["redactEvent"]
+ >[] = [];
+
+ public constructor(
+ private readonly ownUserId: string,
+ private readonly existingRelations: MatrixEvent[] = [],
+ ) {
+ super();
+ }
+
+ public get client(): MatrixClient {
+ return {
+ getUserId: (): string => this.ownUserId,
+ sendEvent: async (
+ ...props: Parameters
+ ): ReturnType => {
+ this.testSentEvents.push(props);
+ return Promise.resolve({ event_id: randomUUID() });
+ },
+ redactEvent: async (
+ ...props: Parameters
+ ): ReturnType => {
+ this.testRedactedEvents.push(props);
+ return Promise.resolve({ event_id: randomUUID() });
+ },
+ decryptEventIfNeeded: async () => {},
+ on() {
+ return this;
+ },
+ off() {
+ return this;
+ },
+ } as unknown as MatrixClient;
+ }
+
+ public get relations(): Room["relations"] {
+ return {
+ getChildEventsForEvent: (membershipEventId: string) => ({
+ getRelations: (): MatrixEvent[] => {
+ return this.existingRelations.filter(
+ (r) =>
+ r.getContent()["m.relates_to"]?.event_id === membershipEventId,
+ );
+ },
+ }),
+ } as unknown as Room["relations"];
+ }
+
+ public testSendHandRaise(
+ parentMemberEvent: string,
+ membershipOrOverridenSender: Record | string,
+ ): string {
+ const evt = createHandRaisedReaction(
+ parentMemberEvent,
+ membershipOrOverridenSender,
+ );
+ this.emit(RoomEvent.Timeline, evt, this, undefined, false, {
+ timeline: new EventTimeline(new EventTimelineSet(undefined)),
+ });
+ return evt.getId()!;
+ }
+
+ public testSendReaction(
+ parentMemberEvent: string,
+ reaction: ReactionOption,
+ membershipOrOverridenSender: Record | string,
+ ): string {
+ const evt = new MatrixEvent({
+ sender:
+ typeof membershipOrOverridenSender === "string"
+ ? membershipOrOverridenSender
+ : membershipOrOverridenSender[parentMemberEvent],
+ type: ElementCallReactionEventType,
+ origin_server_ts: new Date().getTime(),
+ content: {
+ "m.relates_to": {
+ rel_type: RelationType.Reference,
+ event_id: parentMemberEvent,
+ },
+ emoji: reaction.emoji,
+ name: reaction.name,
+ } satisfies ECallReactionEventContent,
+ event_id: randomUUID(),
+ });
+
+ this.emit(RoomEvent.Timeline, evt, this, undefined, false, {
+ timeline: new EventTimeline(new EventTimelineSet(undefined)),
+ });
+ return evt.getId()!;
+ }
+}
diff --git a/src/widget.ts b/src/widget.ts
index 9d3da479..fb1b1cfd 100644
--- a/src/widget.ts
+++ b/src/widget.ts
@@ -15,6 +15,7 @@ import type { IWidgetApiRequest } from "matrix-widget-api";
import { LazyEventEmitter } from "./LazyEventEmitter";
import { getUrlParams } from "./UrlParams";
import { Config } from "./config/Config";
+import { ElementCallReactionEventType } from "./reactions";
// Subset of the actions in matrix-react-sdk
export enum ElementWidgetActions {
@@ -105,6 +106,7 @@ export const widget = ((): WidgetHelpers | null => {
EventType.CallEncryptionKeysPrefix,
EventType.Reaction,
EventType.RoomRedaction,
+ ElementCallReactionEventType,
];
const sendState = [
diff --git a/yarn.lock b/yarn.lock
index 3f98879c..47699474 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1608,6 +1608,15 @@
dependencies:
tslib "2"
+"@formatjs/intl-segmenter@^11.7.3":
+ version "11.7.3"
+ resolved "https://registry.yarnpkg.com/@formatjs/intl-segmenter/-/intl-segmenter-11.7.3.tgz#aeb49c33c81fec68419922c64c72188b659eaa5a"
+ integrity sha512-IvEDQRe0t0ouqaqZK2KobGt/+BhwDHdtbS8GWhdl+fjmWbhXMz2mHihu5fAYkYChum5eNfGhEF5P+bLCeYq67w==
+ dependencies:
+ "@formatjs/ecma402-abstract" "2.2.3"
+ "@formatjs/intl-localematcher" "0.5.7"
+ tslib "2"
+
"@gulpjs/to-absolute-glob@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz#1fc2460d3953e1d9b9f2dfdb4bcc99da4710c021"
@@ -3100,15 +3109,14 @@
dependencies:
"@use-gesture/core" "10.3.1"
-"@vector-im/compound-design-tokens@^1.0.0":
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.9.0.tgz#a3788845110fdcafb1720f633cb060b86f9a1592"
- integrity sha512-09eIRJSiWtAqK605eIu+PfT1ugu7u13gkvfxvfN7kjJMHQOzHSvDxmwADmfIzlV7oBQ8M+5D4KSKHNskvMxWsA==
+"@vector-im/compound-design-tokens@^1.9.1":
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.9.1.tgz#644dc7ca5ca251fd476af2a7c075e9d740c08871"
+ integrity sha512-zjI+PhoNLNrJrLU8whEGjzCuxdqIz6tM0ARYBMS8AG1vC+NlGak6Y21TWnzHT3VINNhnF+PiQ9lFWsU65GydOg==
-"@vector-im/compound-web@^6.0.0":
- version "6.3.2"
- resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-6.3.2.tgz#92e92056b7922474424a4bf4a3068ebb5bade7c2"
- integrity sha512-l1Bz+QFdgnwzJ17fpC9gzbDMHBsfwdb1AX53LOS1p91tdo2pypElGAJDWpaCy90UT900tKjZd+0qwMapdkuQmg==
+"@vector-im/compound-web@element-hq/compound-web#46cf2d94d9c9b6d25e80ef0e785f3a929ed040ea":
+ version "7.1.0"
+ resolved "https://codeload.github.com/element-hq/compound-web/tar.gz/46cf2d94d9c9b6d25e80ef0e785f3a929ed040ea"
dependencies:
"@floating-ui/react" "^0.26.24"
"@radix-ui/react-context-menu" "^2.2.1"
@@ -5990,9 +5998,9 @@ matrix-events-sdk@0.0.1:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
-matrix-js-sdk@matrix-org/matrix-js-sdk#e11f9defb9b9603ee2d982a8fd2b0c9577fbf48c:
+matrix-js-sdk@matrix-org/matrix-js-sdk#6971e7bebaad643c233e5057da7a0d42441c0789:
version "34.10.0"
- resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e11f9defb9b9603ee2d982a8fd2b0c9577fbf48c"
+ resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/6971e7bebaad643c233e5057da7a0d42441c0789"
dependencies:
"@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-wasm" "^9.0.0"
@@ -6010,10 +6018,10 @@ matrix-js-sdk@matrix-org/matrix-js-sdk#e11f9defb9b9603ee2d982a8fd2b0c9577fbf48c:
unhomoglyph "^1.0.6"
uuid "11"
-matrix-widget-api@^1.8.2:
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.9.0.tgz#884136b405bd3c56e4ea285095c9e01ec52b6b1f"
- integrity sha512-au8mqralNDqrEvaVAkU37bXOb8I9SCe+ACdPk11QWw58FKstVq31q2wRz+qWA6J+42KJ6s1DggWbG/S3fEs3jw==
+matrix-widget-api@^1.10.0, matrix-widget-api@^1.8.2:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.10.0.tgz#d31ea073a5871a1fb1a511ef900b0c125a37bf55"
+ integrity sha512-rkAJ29briYV7TJnfBVLVSKtpeBrBju15JZFSDP6wj8YdbCu1bdmlplJayQ+vYaw1x4fzI49Q+Nz3E85s46sRDw==
dependencies:
"@types/events" "^3.0.0"
events "^3.2.0"
@@ -7527,7 +7535,16 @@ streamx@^2.12.0, streamx@^2.12.5, streamx@^2.13.2, streamx@^2.14.0:
optionalDependencies:
bare-events "^2.2.0"
-"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0":
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -7622,7 +7639,14 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -8416,7 +8440,7 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -8434,6 +8458,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"