diff --git a/src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts b/src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts
new file mode 100644
index 00000000..e9d91fe4
--- /dev/null
+++ b/src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts
@@ -0,0 +1,33 @@
+import { Participant } from 'livekit-client'
+import { fetchServerApi } from './fetchServerApi'
+import { buildServerApiUrl } from './buildServerApiUrl'
+import { useRoomData } from '../hooks/useRoomData'
+
+export const useLowerHandParticipant = () => {
+ const data = useRoomData()
+
+ const lowerHandParticipant = (participant: Participant) => {
+ if (!data || !data?.livekit) {
+ throw new Error('Room data is not available')
+ }
+ const newMetadata = JSON.parse(participant.metadata || '{}')
+ newMetadata.raised = !newMetadata.raised
+ return fetchServerApi(
+ buildServerApiUrl(
+ data.livekit.url,
+ 'twirp/livekit.RoomService/UpdateParticipant'
+ ),
+ data.livekit.token,
+ {
+ method: 'POST',
+ body: JSON.stringify({
+ room: data.livekit.room,
+ identity: participant.identity,
+ metadata: JSON.stringify(newMetadata),
+ permission: participant.permissions,
+ }),
+ }
+ )
+ }
+ return { lowerHandParticipant }
+}
diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/HandRaisedListItem.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/HandRaisedListItem.tsx
new file mode 100644
index 00000000..0571d066
--- /dev/null
+++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/HandRaisedListItem.tsx
@@ -0,0 +1,78 @@
+import { css } from '@/styled-system/css'
+
+import { HStack } from '@/styled-system/jsx'
+import { Text } from '@/primitives/Text'
+import { useTranslation } from 'react-i18next'
+import { Avatar } from '@/components/Avatar'
+import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
+import { Participant } from 'livekit-client'
+import { isLocal } from '@/utils/livekit'
+import { RiHand } from '@remixicon/react'
+import { ListItemActionButton } from '@/features/rooms/livekit/components/controls/Participants/ListItemActionButton'
+import { useLowerHandParticipant } from '@/features/rooms/livekit/api/lowerHandParticipant.ts'
+
+type HandRaisedListItemProps = {
+ participant: Participant
+}
+
+export const HandRaisedListItem = ({
+ participant,
+}: HandRaisedListItemProps) => {
+ const { t } = useTranslation('rooms')
+ const name = participant.name || participant.identity
+
+ const { lowerHandParticipant } = useLowerHandParticipant()
+
+ return (
+
+
+
+
+
+ {name}
+
+ {isLocal(participant) && (
+
+ ({t('participants.you')})
+
+ )}
+
+
+ lowerHandParticipant(participant)}
+ tooltip={t('participants.lowerParticipantHand', { name })}
+ >
+
+
+
+ )
+}
diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx
index cbcba0c4..16ca082b 100644
--- a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx
+++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx
@@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next'
import { allParticipantRoomEvents } from '@/features/rooms/livekit/constants/events'
import { ParticipantListItem } from '@/features/rooms/livekit/components/controls/Participants/ParticipantListItem'
import { ParticipantsCollapsableList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList'
+import { HandRaisedListItem } from '@/features/rooms/livekit/components/controls/Participants/HandRaisedListItem'
// TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
export const ParticipantsList = () => {
@@ -35,6 +36,11 @@ export const ParticipantsList = () => {
...sortedRemoteParticipants,
]
+ const raisedHandParticipants = participants.filter((participant) => {
+ const data = JSON.parse(participant.metadata || '{}')
+ return data.raised
+ })
+
// TODO - extract inline styling in a centralized styling file, and avoid magic numbers
return (
{
>
{t('participants.subheading').toUpperCase()}
+ {raisedHandParticipants.length > 0 && (
+
+ )}