Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hand raise feature #2542

Merged
merged 47 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
48cf487
Initial support for Hand Raise feature
mgcm Aug 7, 2024
2d1917c
Refactored to use reaction and redaction events
mgcm Sep 8, 2024
f6ae6a0
Replacing button svg with raised hand emoji
mgcm Sep 8, 2024
ac7321d
SpotlightTile should not duplicate the raised hand
mgcm Sep 8, 2024
bcad500
Update src/room/useRaisedHands.tsx
fkwp Sep 9, 2024
0730ba5
Use relations to load existing reactions when joining the call
mgcm Sep 10, 2024
ab5654c
Links to sha commit of matrix-js-sdk that exposes the call membership…
mgcm Sep 10, 2024
7ac5642
Removing RaiseHand.svg
mgcm Sep 10, 2024
69a50fb
Check for reaction & redaction capabilities in widget mode
mgcm Sep 19, 2024
42a7b1e
Fix failing GridTile test
mgcm Sep 19, 2024
16afb56
Center align hand raise.
Half-Shot Oct 25, 2024
1c8e547
Add support for displaying the duration of a raised hand.
Half-Shot Oct 25, 2024
7f268a3
Add a sound for when a hand is raised.
Half-Shot Oct 25, 2024
a23d256
Refactor raised hand indicator and add tests.
Half-Shot Oct 28, 2024
43b4fc0
lint
Half-Shot Oct 28, 2024
4501e67
Refactor into own files.
Half-Shot Oct 28, 2024
4a712dc
Redact the right thing.
Half-Shot Oct 28, 2024
ba921f8
Tidy up useEffect
Half-Shot Oct 28, 2024
9d01e8c
Lint tests
Half-Shot Oct 28, 2024
38878d3
Remove extra layer
Half-Shot Oct 28, 2024
33724ef
Add better sound. (woosh)
Half-Shot Oct 28, 2024
198859d
Add a small mode for spotlight
Half-Shot Oct 28, 2024
07d3451
Fix timestamp calculation on relaod.
Half-Shot Oct 28, 2024
b7e8236
Fix call border resizing video
Half-Shot Oct 29, 2024
23d849b
lint
Half-Shot Oct 29, 2024
f13bd79
Fix and update tests
Half-Shot Oct 29, 2024
dbabf45
Allow timer to be configurable.
Half-Shot Oct 29, 2024
e1a4310
Add preferences tab for choosing to enable timer.
Half-Shot Oct 29, 2024
0b6cf18
Drop border from raised hand icon
Half-Shot Oct 29, 2024
528e692
Handle cases when a new member event happens.
Half-Shot Oct 29, 2024
cd73ad8
Prevent infinite loop
Half-Shot Oct 29, 2024
5a5c1be
Major refactor to support various state problems.
Half-Shot Oct 29, 2024
ff7da13
Tidy up and finish test rewrites
Half-Shot Oct 29, 2024
a45b01d
Add some explanation comments.
Half-Shot Oct 29, 2024
3229498
Even more comments.
Half-Shot Oct 29, 2024
2d95d4f
Use proper duration formatter
Half-Shot Oct 31, 2024
7229f4b
Remove rerender
Half-Shot Oct 31, 2024
a354a40
Fix redactions not working because they pick up events in transit.
Half-Shot Oct 31, 2024
e49eb55
More tidying
Half-Shot Oct 31, 2024
ec9dec8
Use deferred value
Half-Shot Oct 31, 2024
21380c7
linting
Half-Shot Oct 31, 2024
a9e6aa3
Add tests for cases where we got a reaction from someone else.
Half-Shot Oct 31, 2024
167caa3
Merge remote-tracking branch 'origin/livekit' into raise-hand-button
Half-Shot Oct 31, 2024
748cc58
Be even less brittle.
Half-Shot Nov 1, 2024
f54e1e2
Transpose border to GridTile.
Half-Shot Nov 1, 2024
2d41bf7
Merge branch 'livekit' into raise-hand-button
Half-Shot Nov 4, 2024
81fbdfc
lint
Half-Shot Nov 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -144,6 +146,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 { 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 @@
// '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 @@
saveSession({ ...session, passwordlessUser: false });

setInitClientState({
client: initClientState.client,
...initClientState,

Check warning on line 195 in src/ClientContext.tsx

View check run for this annotation

Codecov / codecov/patch

src/ClientContext.tsx#L195

Added line #L195 was not covered by tests
passwordlessUser: false,
});
},
[initClientState?.client],
[initClientState],

Check warning on line 199 in src/ClientContext.tsx

View check run for this annotation

Codecov / codecov/patch

src/ClientContext.tsx#L199

Added line #L199 was not covered by tests
);

const setClient = useCallback(
Expand All @@ -206,6 +210,7 @@
if (clientParams) {
saveSession(clientParams.session);
setInitClientState({
widgetApi: null,

Check warning on line 213 in src/ClientContext.tsx

View check run for this annotation

Codecov / codecov/patch

src/ClientContext.tsx#L213

Added line #L213 was not covered by tests
client: clientParams.client,
passwordlessUser: clientParams.session.passwordlessUser,
});
Expand Down Expand Up @@ -254,6 +259,7 @@
);

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

Check warning on line 262 in src/ClientContext.tsx

View check run for this annotation

Codecov / codecov/patch

src/ClientContext.tsx#L262

Added line #L262 was not covered by tests

const state: ClientState | undefined = useMemo(() => {
if (alreadyOpenedErr) {
Expand All @@ -277,6 +283,9 @@
authenticated,
setClient,
disconnected: isDisconnected,
supportedFeatures: {
reactions: supportsReactions,
},

Check warning on line 288 in src/ClientContext.tsx

View check run for this annotation

Codecov / codecov/patch

src/ClientContext.tsx#L286-L288

Added lines #L286 - L288 were not covered by tests
};
}, [
alreadyOpenedErr,
Expand All @@ -285,6 +294,7 @@
logout,
setClient,
isDisconnected,
supportsReactions,

Check warning on line 297 in src/ClientContext.tsx

View check run for this annotation

Codecov / codecov/patch

src/ClientContext.tsx#L297

Added line #L297 was not covered by tests
]);

const onSync = useCallback(
Expand All @@ -309,6 +319,30 @@
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",
);

Check warning on line 334 in src/ClientContext.tsx

View check run for this annotation

Codecov / codecov/patch

src/ClientContext.tsx#L322-L334

Added lines #L322 - L334 were not covered by tests

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

Check warning on line 344 in src/ClientContext.tsx

View check run for this annotation

Codecov / codecov/patch

src/ClientContext.tsx#L336-L344

Added lines #L336 - L344 were not covered by tests

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

type InitResult = {
widgetApi: WidgetApi | null;
client: MatrixClient;
passwordlessUser: boolean;
};
Expand All @@ -336,6 +371,7 @@
logger.log("Using a matryoshka client");
const client = await widget.client;
return {
widgetApi: widget.api,

Check warning on line 374 in src/ClientContext.tsx

View check run for this annotation

Codecov / codecov/patch

src/ClientContext.tsx#L374

Added line #L374 was not covered by tests
client,
passwordlessUser: false,
};
Expand Down Expand Up @@ -364,6 +400,7 @@
try {
const client = await initClient(initClientParams, true);
return {
widgetApi: null,

Check warning on line 403 in src/ClientContext.tsx

View check run for this annotation

Codecov / codecov/patch

src/ClientContext.tsx#L403

Added line #L403 was not covered by tests
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
137 changes: 137 additions & 0 deletions src/button/RaisedHandToggleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
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 InnerButtonButtonProps extends ComponentPropsWithoutRef<"button"> {
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
raised: boolean;
}
const InnerButton: FC<InnerButtonButtonProps> = ({ raised, ...props }) => {
const { t } = useTranslation();

Check warning on line 29 in src/button/RaisedHandToggleButton.tsx

View check run for this annotation

Codecov / codecov/patch

src/button/RaisedHandToggleButton.tsx#L29

Added line #L29 was not covered by tests

return (
<Tooltip label={t("common.raise_hand")}>
<CpdButton
kind={raised ? "primary" : "secondary"}
{...props}
style={{ paddingLeft: 8, paddingRight: 8 }}

Check warning on line 36 in src/button/RaisedHandToggleButton.tsx

View check run for this annotation

Codecov / codecov/patch

src/button/RaisedHandToggleButton.tsx#L31-L36

Added lines #L31 - L36 were not covered by tests
>
<p
role="img"
aria-label="raised hand"
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
style={{
width: "30px",
height: "0px",
display: "inline-block",
fontSize: "22px",
}}
>

Check warning on line 47 in src/button/RaisedHandToggleButton.tsx

View check run for this annotation

Codecov / codecov/patch

src/button/RaisedHandToggleButton.tsx#L38-L47

Added lines #L38 - L47 were not covered by tests
</p>
</CpdButton>
</Tooltip>

Check warning on line 51 in src/button/RaisedHandToggleButton.tsx

View check run for this annotation

Codecov / codecov/patch

src/button/RaisedHandToggleButton.tsx#L49-L51

Added lines #L49 - L51 were not covered by tests
);
};

Check warning on line 53 in src/button/RaisedHandToggleButton.tsx

View check run for this annotation

Codecov / codecov/patch

src/button/RaisedHandToggleButton.tsx#L53

Added line #L53 was not covered by tests

interface RaisedHandToggleButton {
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
rtcSession: MatrixRTCSession;
client: MatrixClient;
}

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

Check warning on line 69 in src/button/RaisedHandToggleButton.tsx

View check run for this annotation

Codecov / codecov/patch

src/button/RaisedHandToggleButton.tsx#L61-L69

Added lines #L61 - L69 were not covered by tests

const toggleRaisedHand = useCallback(() => {
if (isHandRaised) {
if (myReactionId) {
setBusy(true);
client
.redactEvent(rtcSession.room.roomId, myReactionId)
.then(() => {
logger.debug("Redacted raise hand event");
removeRaisedHand(userId);
Half-Shot marked this conversation as resolved.
Show resolved Hide resolved
})
.catch((e) => {
logger.error("Failed to redact reaction event", e);
})
.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;
setBusy(true);
client
.sendEvent(rtcSession.room.roomId, EventType.Reaction, {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: parentEventId,
key: "🖐️",
},
})
.then((reaction) => {
logger.debug("Sent raise hand event", reaction.event_id);
addRaisedHand(userId, {
membershipEventId: parentEventId,
reactionEventId: reaction.event_id,
time: new Date(),
});
})
.catch((e) => {
logger.error("Failed to send reaction event", e);
})
.finally(() => {
setBusy(false);
});
}
}, [
client,
isHandRaised,
memberships,
myReactionId,
rtcSession.room.roomId,
addRaisedHand,
removeRaisedHand,
userId,
]);

Check warning on line 128 in src/button/RaisedHandToggleButton.tsx

View check run for this annotation

Codecov / codecov/patch

src/button/RaisedHandToggleButton.tsx#L71-L128

Added lines #L71 - L128 were not covered by tests

return (
<InnerButton
disabled={busy}
onClick={toggleRaisedHand}
raised={isHandRaised}
/>

Check warning on line 135 in src/button/RaisedHandToggleButton.tsx

View check run for this annotation

Codecov / codecov/patch

src/button/RaisedHandToggleButton.tsx#L130-L135

Added lines #L130 - L135 were not covered by tests
);
}

Check warning on line 137 in src/button/RaisedHandToggleButton.tsx

View check run for this annotation

Codecov / codecov/patch

src/button/RaisedHandToggleButton.tsx#L137

Added line #L137 was not covered by tests
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