(frontend) add raised hands waiting room

Show the raised hand waiting list, allow any participant to
lower other participants' hands.

Inspired from Gmeet. I found it quite clear to have a dedicated
waiting list for raised hands.
This commit is contained in:
lebaudantoine
2024-09-03 09:59:21 +02:00
committed by aleb_the_flash
parent 59ec88e84a
commit 584be7e65b
6 changed files with 141 additions and 3 deletions

View File

@@ -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 }
}

View File

@@ -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 (
<HStack
role="listitem"
justify="space-between"
key={participant.identity}
id={participant.identity}
className={css({
padding: '0.25rem 0',
width: 'full',
})}
>
<HStack>
<Avatar name={name} bgColor={getParticipantColor(participant)} />
<Text
variant={'sm'}
className={css({
userSelect: 'none',
cursor: 'default',
display: 'flex',
})}
>
<span
className={css({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '120px',
display: 'block',
})}
>
{name}
</span>
{isLocal(participant) && (
<span
className={css({
marginLeft: '.25rem',
whiteSpace: 'nowrap',
})}
>
({t('participants.you')})
</span>
)}
</Text>
</HStack>
<ListItemActionButton
onPress={() => lowerHandParticipant(participant)}
tooltip={t('participants.lowerParticipantHand', { name })}
>
<RiHand color="gray" />
</ListItemActionButton>
</HStack>
)
}

View File

@@ -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 (
<Box
@@ -83,6 +89,21 @@ export const ParticipantsList = () => {
>
{t('participants.subheading').toUpperCase()}
</H>
{raisedHandParticipants.length > 0 && (
<div
style={{
marginBottom: '.9375rem',
}}
>
<ParticipantsCollapsableList
heading={t('participants.raisedHands')}
participants={raisedHandParticipants}
renderParticipant={(participant) => (
<HandRaisedListItem participant={participant} />
)}
/>
</div>
)}
<ParticipantsCollapsableList
heading={t('participants.contributors')}
participants={sortedParticipants}

View File

@@ -66,6 +66,8 @@
"description": "",
"confirm": "",
"cancel": ""
}
},
"raisedHands": "",
"lowerParticipantHand": ""
}
}

View File

@@ -66,6 +66,8 @@
"description": "Mute {{name}} for all participants? {{name}} is the only person who can unmute themselves.",
"confirm": "Mute",
"cancel": "Cancel"
}
},
"raisedHands": "Raised hands",
"lowerParticipantHand": "Lower {{name}}'s hand"
}
}

View File

@@ -66,6 +66,8 @@
"description": "Couper le micro de {{name}} pour tous les participants ? {{name}} est la seule personne habilitée à réactiver son micro",
"confirm": "Couper le micro",
"cancel": "Annuler"
}
},
"raisedHands": "Mains levées",
"lowerParticipantHand": "Baisser la main de {{name}}"
}
}