Support for generic reactions (#2708)

* Initial support for Hand Raise feature

Signed-off-by: Milton Moura <miltonmoura@gmail.com>

* Refactored to use reaction and redaction events

Signed-off-by: Milton Moura <miltonmoura@gmail.com>

* Replacing button svg with raised hand emoji

Signed-off-by: Milton Moura <miltonmoura@gmail.com>

* SpotlightTile should not duplicate the raised hand

Signed-off-by: Milton Moura <miltonmoura@gmail.com>

* Update src/room/useRaisedHands.tsx

Element Call recently changed to AGPL-3.0

* Use relations to load existing reactions when joining the call

Signed-off-by: Milton Moura <miltonmoura@gmail.com>

* Links to sha commit of matrix-js-sdk that exposes the call membership event id and refactors some async code

Signed-off-by: Milton Moura <miltonmoura@gmail.com>

* Removing RaiseHand.svg

* Check for reaction & redaction capabilities in widget mode

Signed-off-by: Milton Moura <miltonmoura@gmail.com>

* Fix failing GridTile test

Signed-off-by: Milton Moura <miltonmoura@gmail.com>

* Center align hand raise.

* Add support for displaying the duration of a raised hand.

* Add a sound for when a hand is raised.

* Refactor raised hand indicator and add tests.

* lint

* Refactor into own files.

* Redact the right thing.

* Tidy up useEffect

* Lint tests

* Remove extra layer

* Add better sound. (woosh)

* Add a small mode for spotlight

* Fix timestamp calculation on relaod.

* Fix call border resizing video

* lint

* Fix and update tests

* Allow timer to be configurable.

* Add preferences tab for choosing to enable timer.

* Drop border from raised hand icon

* Handle cases when a new member event happens.

* Prevent infinite loop

* Major refactor to support various state problems.

* Tidy up and finish test rewrites

* Add some explanation comments.

* Even more comments.

* Use proper duration formatter

* Remove rerender

* Fix redactions not working because they pick up events in transit.

* More tidying

* Use deferred value

* linting

* Add tests for cases where we got a reaction from someone else.

* Be even less brittle.

* Transpose border to GridTile.

* First PoC for reactions

* hide menu by default

* Add lightbulb.

* Add reaction indicator.

* Add sounds.

* Tidy up + add support for floating emoji.

* Linting and general stability improvements.

* Subscribe to the ecall reaction event type.

* fix import

* Center emoji picker

* Overflow buttons when screen is too narrow

* lint

* Add settings for disabling animations / sounds.

* Make vertical divider more visually distinct.

* Make event listener more resillient.

* lint

* Fix some tests.

* Remove old raised hand component

* Add new icon

* Update text

* Update compound hand raised icon.

* Add deer.

* Fix case where you could send larger strings as emoji

* Const the active time.

* Document time in css.

* Add rock emoji

* Add licence file.

* Add type def for custom reaction type.

* better reaction description

* Factor out reactions test structure to utils file.

* Add tests for ReactionToggleButton

* Add keyboard shortcuts for reaction sending.

* type tidyups

* lint

* Add tests for ReactionAudioRenderer

* lint

* prettier

* i18n sort

* final lint?

* Preload reaction sounds to prevent delays.

* Update rock sounds

* add onclick back

* Fix test

* lint

* simplify

* Tweak line height

* modal impl

* Modal refactor attempts.

* Remove closed menu test since we're using Modal.

* Swap icon, make mobile view better.

* Fix mobile view for emoji picker.

* Use Intl.Segmenter

* Clear timeouts on component close.

* Remove useless useCallback

* Use prefers-reduced-motion

* Add toggle for raise hand.

* Add lower hand text

* Add lower motion mode.

* Decomplicate className system for Modal

* Add error for failured to send reaction.

* i18n

* Spacing for emoji buttons search

* Remove unrequired media query

* Fix generic sound not playing.

* Clear reactions if we're clearing timeouts.

* Fix tests

* Relabel lower hand

* More translations

* Add comments on reaction interface

* Move polyfill.

* lint

* Replace deer sound

* Another attempt to fix the sizing of the reactions

* cleanup

* fix button

* fix

---------

Signed-off-by: Milton Moura <miltonmoura@gmail.com>
Co-authored-by: Milton Moura <miltonmoura@gmail.com>
Co-authored-by: fkwp <fkwp@users.noreply.github.com>
This commit is contained in:
Will Hunt
2024-11-08 17:36:40 +00:00
committed by GitHub
parent 5b94dd6f1a
commit 5d88c52e30
48 changed files with 2000 additions and 387 deletions

199
src/utils/testReactions.tsx Normal file
View File

@@ -0,0 +1,199 @@
/*
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 (
<ReactionsProvider rtcSession={rtcSession as unknown as MatrixRTCSession}>
{children}
</ReactionsProvider>
);
};
export class MockRTCSession extends EventEmitter {
public memberships: {
sender: string;
eventId: string;
createdTs: () => Date;
}[];
public constructor(
public readonly room: MockRoom,
membership: Record<string, string>,
) {
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, string> | 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<MatrixClient["sendEvent"]>[] = [];
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<MatrixClient["sendEvent"]>
): ReturnType<MatrixClient["sendEvent"]> => {
this.testSentEvents.push(props);
return Promise.resolve({ event_id: randomUUID() });
},
redactEvent: async (
...props: Parameters<MatrixClient["redactEvent"]>
): ReturnType<MatrixClient["redactEvent"]> => {
this.testRedactedEvents.push(props);
return Promise.resolve({ event_id: randomUUID() });
},
} 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> | 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> | 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()!;
}
}