diff --git a/src/reactions/RaisedHandIndicator.module.css b/src/reactions/RaisedHandIndicator.module.css
new file mode 100644
index 00000000..b5dd6e49
--- /dev/null
+++ b/src/reactions/RaisedHandIndicator.module.css
@@ -0,0 +1,39 @@
+.raisedHandWidget {
+ display: flex;
+ background-color: var(--cpd-color-bg-subtle-primary);
+ border-radius: var(--cpd-radius-pill-effect);
+ color: var(--cpd-color-icon-secondary);
+ border: 1px solid var(--cpd-color-yellow-1200);
+}
+
+.raisedHandWidget > p {
+ padding: var(--cpd-space-2x);
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 4em;
+}
+
+.raisedHand {
+ margin: var(--cpd-space-2x);
+ padding: var(--cpd-space-2x);
+ padding-block: var(--cpd-space-2x);
+ 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-8x);
+ height: var(--cpd-space-8x);
+ display: inline-block;
+ text-align: center;
+ font-size: 22px;
+}
diff --git a/src/reactions/RaisedHandIndicator.test.tsx b/src/reactions/RaisedHandIndicator.test.tsx
new file mode 100644
index 00000000..8463a625
--- /dev/null
+++ b/src/reactions/RaisedHandIndicator.test.tsx
@@ -0,0 +1,36 @@
+/*
+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", async () => {
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ });
+ test("renders an indicator when a hand has been raised", async () => {
+ const dateTime = new Date();
+ const { container } = render(
+ ,
+ );
+ expect(container.firstChild).toMatchSnapshot();
+ });
+ test("renders an indicator when a hand has been raised with the expected time", async () => {
+ const dateTime = new Date(new Date().getTime() - 60000);
+ const { container } = render(
+ ,
+ );
+ expect(container.firstChild).toMatchSnapshot();
+ });
+});
diff --git a/src/reactions/RaisedHandIndicator.tsx b/src/reactions/RaisedHandIndicator.tsx
new file mode 100644
index 00000000..012fa571
--- /dev/null
+++ b/src/reactions/RaisedHandIndicator.tsx
@@ -0,0 +1,44 @@
+import { useEffect, useState } from "react";
+import styles from "./RaisedHandIndicator.module.css";
+
+export function RaisedHandIndicator({
+ raisedHandTime,
+}: {
+ raisedHandTime?: Date;
+}) {
+ const [raisedHandDuration, setRaisedHandDuration] = useState("");
+
+ useEffect(() => {
+ if (!raisedHandTime) {
+ return;
+ }
+ const calculateTime = () => {
+ const totalSeconds = Math.ceil(
+ (new Date().getTime() - raisedHandTime.getTime()) / 1000,
+ );
+ const seconds = totalSeconds % 60;
+ const minutes = Math.floor(totalSeconds / 60);
+ setRaisedHandDuration(
+ `${minutes < 10 ? "0" : ""}${minutes}:${seconds < 10 ? "0" : ""}${seconds}`,
+ );
+ };
+ const to = setInterval(calculateTime, 1000);
+ calculateTime();
+ return (): void => clearInterval(to);
+ }, [setRaisedHandDuration, raisedHandTime]);
+
+ if (raisedHandTime) {
+ return (
+
+
+
+ ✋
+
+
+
{raisedHandDuration}
+
+ );
+ }
+
+ return null;
+}
diff --git a/src/reactions/__snapshots__/RaisedHandIndicator.test.tsx.snap b/src/reactions/__snapshots__/RaisedHandIndicator.test.tsx.snap
new file mode 100644
index 00000000..67719949
--- /dev/null
+++ b/src/reactions/__snapshots__/RaisedHandIndicator.test.tsx.snap
@@ -0,0 +1,41 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`RaisedHandIndicator > renders an indicator when a hand has been raised 1`] = `
+
+`;
+
+exports[`RaisedHandIndicator > renders an indicator when a hand has been raised with the expected time 1`] = `
+
+`;
diff --git a/src/tile/MediaView.module.css b/src/tile/MediaView.module.css
index 2dde20a7..c94c1298 100644
--- a/src/tile/MediaView.module.css
+++ b/src/tile/MediaView.module.css
@@ -90,46 +90,6 @@ unconditionally select the container so we can use cqmin units */
place-items: start;
}
-.raisedHandWidget {
- display: flex;
- background-color: var(--cpd-color-bg-subtle-primary);
- border-radius: var(--cpd-radius-pill-effect);
- color: var(--cpd-color-icon-secondary);
- border: 1px solid var(--cpd-color-yellow-1200);
-}
-
-.raisedHandWidget > p {
- padding: var(--cpd-space-2x);
- margin-top: auto;
- margin-bottom: auto;
- width: 4em;
-}
-
-.raisedHand {
- margin: var(--cpd-space-2x);
- padding: var(--cpd-space-2x);
- padding-block: var(--cpd-space-2x);
- 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-8x);
- height: var(--cpd-space-8x);
- display: inline-block;
- text-align: center;
- font-size: 22px;
-}
-
.raisedHandBorder {
border: var(--cpd-space-1x) solid var(--cpd-color-yellow-1200);
}
diff --git a/src/tile/MediaView.tsx b/src/tile/MediaView.tsx
index 85bb79fd..41a3bdc1 100644
--- a/src/tile/MediaView.tsx
+++ b/src/tile/MediaView.tsx
@@ -23,6 +23,7 @@ import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import styles from "./MediaView.module.css";
import { Avatar } from "../Avatar";
+import { RaisedHandIndicator } from "../reactions/RaisedHandIndicator";
interface Props extends ComponentProps {
className?: string;
@@ -64,26 +65,6 @@ export const MediaView = forwardRef(
) => {
const { t } = useTranslation();
- const [raisedHandDuration, setRaisedHandDuration] = useState("");
-
- useEffect(() => {
- if (!raisedHandTime) {
- return;
- }
- setRaisedHandDuration("00:00");
- const to = setInterval(() => {
- const totalSeconds = Math.ceil(
- (new Date().getTime() - raisedHandTime.getTime()) / 1000,
- );
- const seconds = totalSeconds % 60;
- const minutes = Math.floor(totalSeconds / 60);
- setRaisedHandDuration(
- `${minutes < 10 ? "0" : ""}${minutes}:${seconds < 10 ? "0" : ""}${seconds}`,
- );
- }, 1000);
- return (): void => clearInterval(to);
- }, [setRaisedHandDuration, raisedHandTime]);
-
return (
(
)}
- {raisedHandTime && (
-
-
-
- ✋
-
-
-
{raisedHandDuration}
-
- )}
+
{nameTagLeadingIcon}