Skip to content

Commit

Permalink
Hand raise feature (#2542)
Browse files Browse the repository at this point in the history
* Initial support for Hand Raise feature

Signed-off-by: Milton Moura <[email protected]>

* Refactored to use reaction and redaction events

Signed-off-by: Milton Moura <[email protected]>

* Replacing button svg with raised hand emoji

Signed-off-by: Milton Moura <[email protected]>

* SpotlightTile should not duplicate the raised hand

Signed-off-by: Milton Moura <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* Removing RaiseHand.svg

* Check for reaction & redaction capabilities in widget mode

Signed-off-by: Milton Moura <[email protected]>

* Fix failing GridTile test

Signed-off-by: Milton Moura <[email protected]>

* 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.

* lint

---------

Signed-off-by: Milton Moura <[email protected]>
Co-authored-by: fkwp <[email protected]>
Co-authored-by: Half-Shot <[email protected]>
Co-authored-by: Will Hunt <[email protected]>
  • Loading branch information
4 people authored Nov 4, 2024
1 parent f2ed07c commit 1897210
Show file tree
Hide file tree
Showing 24 changed files with 1,149 additions and 30 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@babel/preset-env": "^7.22.20",
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.23.0",
"@formatjs/intl-durationformat": "^0.6.1",
"@livekit/components-core": "^0.11.0",
"@livekit/components-react": "^2.0.0",
"@opentelemetry/api": "^1.4.0",
Expand Down
6 changes: 6 additions & 0 deletions public/locales/en-GB/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
"next": "Next",
"options": "Options",
"password": "Password",
"preferences": "Preferences",
"profile": "Profile",
"raise_hand": "Raise hand",
"settings": "Settings",
"unencrypted": "Not encrypted",
"username": "Username",
Expand Down Expand Up @@ -145,6 +147,10 @@
"feedback_tab_title": "Feedback",
"more_tab_title": "More",
"opt_in_description": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.",
"preferences_tab_body": "Here you can configure extra options for an improved experience",
"preferences_tab_h4": "Preferences",
"preferences_tab_show_hand_raised_timer_description": "Show a timer when a participant raises their hand",
"preferences_tab_show_hand_raised_timer_label": "Show hand raise duration",
"speaker_device_selection_label": "Speaker"
},
"star_rating_input_label_one": "{{count}} stars",
Expand Down
41 changes: 39 additions & 2 deletions src/ClientContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { useTranslation } from "react-i18next";
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
import { MatrixError } from "matrix-js-sdk/src/matrix";
import { WidgetApi } from "matrix-widget-api";

import { ErrorView } from "./FullScreenView";
import { fallbackICEServerAllowed, initClient } from "./utils/matrix";
Expand Down Expand Up @@ -52,6 +53,9 @@ export type ValidClientState = {
// 'Disconnected' rather than 'connected' because it tracks specifically
// whether the client is supposed to be connected but is not
disconnected: boolean;
supportedFeatures: {
reactions: boolean;
};
setClient: (params?: SetClientParams) => void;
};

Expand Down Expand Up @@ -188,11 +192,11 @@ export const ClientProvider: FC<Props> = ({ children }) => {
saveSession({ ...session, passwordlessUser: false });

setInitClientState({
client: initClientState.client,
...initClientState,
passwordlessUser: false,
});
},
[initClientState?.client],
[initClientState],
);

const setClient = useCallback(
Expand All @@ -206,6 +210,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
if (clientParams) {
saveSession(clientParams.session);
setInitClientState({
widgetApi: null,
client: clientParams.client,
passwordlessUser: clientParams.session.passwordlessUser,
});
Expand Down Expand Up @@ -254,6 +259,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
);

const [isDisconnected, setIsDisconnected] = useState(false);
const [supportsReactions, setSupportsReactions] = useState(false);

const state: ClientState | undefined = useMemo(() => {
if (alreadyOpenedErr) {
Expand All @@ -277,6 +283,9 @@ export const ClientProvider: FC<Props> = ({ children }) => {
authenticated,
setClient,
disconnected: isDisconnected,
supportedFeatures: {
reactions: supportsReactions,
},
};
}, [
alreadyOpenedErr,
Expand All @@ -285,6 +294,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
logout,
setClient,
isDisconnected,
supportsReactions,
]);

const onSync = useCallback(
Expand All @@ -309,6 +319,30 @@ export const ClientProvider: FC<Props> = ({ children }) => {
initClientState.client.on(ClientEvent.Sync, onSync);
}

if (initClientState.widgetApi) {
const reactSend = initClientState.widgetApi.hasCapability(
"org.matrix.msc2762.send.event:m.reaction",
);
const redactSend = initClientState.widgetApi.hasCapability(
"org.matrix.msc2762.send.event:m.room.redaction",
);
const reactRcv = initClientState.widgetApi.hasCapability(
"org.matrix.msc2762.receive.event:m.reaction",
);
const redactRcv = initClientState.widgetApi.hasCapability(
"org.matrix.msc2762.receive.event:m.room.redaction",
);

if (!reactSend || !reactRcv || !redactSend || !redactRcv) {
logger.warn("Widget does not support reactions");
setSupportsReactions(false);
} else {
setSupportsReactions(true);
}
} else {
setSupportsReactions(true);
}

return (): void => {
if (initClientState.client) {
initClientState.client.removeListener(ClientEvent.Sync, onSync);
Expand All @@ -326,6 +360,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
};

type InitResult = {
widgetApi: WidgetApi | null;
client: MatrixClient;
passwordlessUser: boolean;
};
Expand All @@ -336,6 +371,7 @@ async function loadClient(): Promise<InitResult | null> {
logger.log("Using a matryoshka client");
const client = await widget.client;
return {
widgetApi: widget.api,
client,
passwordlessUser: false,
};
Expand Down Expand Up @@ -364,6 +400,7 @@ async function loadClient(): Promise<InitResult | null> {
try {
const client = await initClient(initClientParams, true);
return {
widgetApi: null,
client,
passwordlessUser,
};
Expand Down
2 changes: 1 addition & 1 deletion src/Modal.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Please see LICENSE in the repository root for full details.

.dialog {
box-sizing: border-box;
inline-size: 520px;
inline-size: 580px;
max-inline-size: 90%;
max-block-size: 600px;
}
Expand Down
133 changes: 133 additions & 0 deletions src/button/RaisedHandToggleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { Button as CpdButton, Tooltip } from "@vector-im/compound-web";
import {
ComponentPropsWithoutRef,
FC,
ReactNode,
useCallback,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { logger } from "matrix-js-sdk/src/logger";
import { EventType, RelationType } from "matrix-js-sdk/src/matrix";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";

import { useReactions } from "../useReactions";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";

interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> {
raised: boolean;
}

const InnerButton: FC<InnerButtonProps> = ({ raised, ...props }) => {
const { t } = useTranslation();

return (
<Tooltip label={t("common.raise_hand")}>
<CpdButton
kind={raised ? "primary" : "secondary"}
{...props}
style={{ paddingLeft: 8, paddingRight: 8 }}
>
<p
role="img"
aria-hidden
style={{
width: "30px",
height: "0px",
display: "inline-block",
fontSize: "22px",
}}
>
</p>
</CpdButton>
</Tooltip>
);
};

interface RaisedHandToggleButtonProps {
rtcSession: MatrixRTCSession;
client: MatrixClient;
}

export function RaiseHandToggleButton({
client,
rtcSession,
}: RaisedHandToggleButtonProps): ReactNode {
const { raisedHands, myReactionId } = useReactions();
const [busy, setBusy] = useState(false);
const userId = client.getUserId()!;
const isHandRaised = !!raisedHands[userId];
const memberships = useMatrixRTCSessionMemberships(rtcSession);

const toggleRaisedHand = useCallback(() => {
const raiseHand = async (): Promise<void> => {
if (isHandRaised) {
if (!myReactionId) {
logger.warn(`Hand raised but no reaction event to redact!`);
return;
}
try {
setBusy(true);
await client.redactEvent(rtcSession.room.roomId, myReactionId);
logger.debug("Redacted raise hand event");
} catch (ex) {
logger.error("Failed to redact reaction event", myReactionId, ex);
} finally {
setBusy(false);
}
} else {
const myMembership = memberships.find((m) => m.sender === userId);
if (!myMembership?.eventId) {
logger.error("Cannot find own membership event");
return;
}
const parentEventId = myMembership.eventId;
try {
setBusy(true);
const reaction = await client.sendEvent(
rtcSession.room.roomId,
EventType.Reaction,
{
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: parentEventId,
key: "🖐️",
},
},
);
logger.debug("Sent raise hand event", reaction.event_id);
} catch (ex) {
logger.error("Failed to send reaction event", ex);
} finally {
setBusy(false);
}
}
};

void raiseHand();
}, [
client,
isHandRaised,
memberships,
myReactionId,
rtcSession.room.roomId,
userId,
]);

return (
<InnerButton
disabled={busy}
onClick={toggleRaisedHand}
raised={isHandRaised}
/>
);
}
1 change: 1 addition & 0 deletions src/button/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Please see LICENSE in the repository root for full details.

export * from "./Button";
export * from "./LinkButton";
export * from "./RaisedHandToggleButton";
52 changes: 52 additions & 0 deletions src/reactions/RaisedHandIndicator.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.raisedHandWidget {
display: flex;
background-color: var(--cpd-color-bg-subtle-primary);
border-radius: var(--cpd-radius-pill-effect);
color: var(--cpd-color-icon-secondary);
}

.raisedHandWidget > p {
padding: none;
margin-top: auto;
margin-bottom: auto;
width: 4em;
}

.raisedHandWidgetLarge > p {
padding: var(--cpd-space-2x);
}

.raisedHandLarge {
margin: var(--cpd-space-2x);
padding: var(--cpd-space-2x);
padding-block: var(--cpd-space-2x);
}

.raisedHand {
margin: var(--cpd-space-1x);
color: var(--cpd-color-icon-secondary);
background-color: var(--cpd-color-icon-secondary);
display: flex;
align-items: center;
border-radius: var(--cpd-radius-pill-effect);
user-select: none;
overflow: hidden;
box-shadow: var(--small-drop-shadow);
box-sizing: border-box;
max-inline-size: 100%;
max-width: fit-content;
}

.raisedHand > span {
width: var(--cpd-space-6x);
height: var(--cpd-space-6x);
display: inline-block;
text-align: center;
font-size: 16px;
}

.raisedHandLarge > span {
width: var(--cpd-space-8x);
height: var(--cpd-space-8x);
font-size: 22px;
}
43 changes: 43 additions & 0 deletions src/reactions/RaisedHandIndicator.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { describe, expect, test } from "vitest";
import { render, configure } from "@testing-library/react";

import { RaisedHandIndicator } from "./RaisedHandIndicator";

configure({
defaultHidden: true,
});

describe("RaisedHandIndicator", () => {
test("renders nothing when no hand has been raised", () => {
const { container } = render(<RaisedHandIndicator />);
expect(container.firstChild).toBeNull();
});
test("renders an indicator when a hand has been raised", () => {
const dateTime = new Date();
const { container } = render(
<RaisedHandIndicator raisedHandTime={dateTime} showTimer />,
);
expect(container.firstChild).toMatchSnapshot();
});
test("renders an indicator when a hand has been raised with the expected time", () => {
const dateTime = new Date(new Date().getTime() - 60000);
const { container } = render(
<RaisedHandIndicator raisedHandTime={dateTime} showTimer />,
);
expect(container.firstChild).toMatchSnapshot();
});
test("renders a smaller indicator when minature is specified", () => {
const dateTime = new Date();
const { container } = render(
<RaisedHandIndicator raisedHandTime={dateTime} minature showTimer />,
);
expect(container.firstChild).toMatchSnapshot();
});
});
Loading

0 comments on commit 1897210

Please sign in to comment.