(frontend) display position of a raised hand in the queue

Users requested an enhanced visual indicator
for raised hands on the participant tile.

Most major video conferencing platforms display the position of a raised hand
in the queue. This helps hosts quickly see who is requesting to speak,
and in what order, without needing to open the full participant list.

While a minor feature, this improvement is especially valuable for power user
This commit is contained in:
lebaudantoine
2025-08-01 12:35:42 +02:00
committed by aleb_the_flash
parent 1db189ace2
commit 965d823d08
2 changed files with 56 additions and 14 deletions

View File

@@ -22,7 +22,7 @@ import {
} from '@livekit/components-core' } from '@livekit/components-core'
import { Track } from 'livekit-client' import { Track } from 'livekit-client'
import { RiHand } from '@remixicon/react' import { RiHand } from '@remixicon/react'
import { useRaisedHand } from '../hooks/useRaisedHand' import { useRaisedHand, useRaisedHandPosition } from '../hooks/useRaisedHand'
import { HStack } from '@/styled-system/jsx' import { HStack } from '@/styled-system/jsx'
import { MutedMicIndicator } from './MutedMicIndicator' import { MutedMicIndicator } from './MutedMicIndicator'
import { ParticipantPlaceholder } from './ParticipantPlaceholder' import { ParticipantPlaceholder } from './ParticipantPlaceholder'
@@ -97,6 +97,10 @@ export const ParticipantTile: (
participant: trackReference.participant, participant: trackReference.participant,
}) })
const { positionInQueue, firstInQueue } = useRaisedHandPosition({
participant: trackReference.participant,
})
const isScreenShare = trackReference.source != Track.Source.Camera const isScreenShare = trackReference.source != Track.Source.Camera
return ( return (
@@ -141,24 +145,32 @@ export const ParticipantTile: (
style={{ style={{
padding: '0.1rem 0.25rem', padding: '0.1rem 0.25rem',
backgroundColor: backgroundColor:
isHandRaised && !isScreenShare ? 'white' : undefined, isHandRaised && !isScreenShare
? firstInQueue
? '#fde047'
: 'white'
: undefined,
color: color:
isHandRaised && !isScreenShare ? 'black' : undefined, isHandRaised && !isScreenShare ? 'black' : undefined,
transition: 'background 200ms ease, color 400ms ease', transition: 'background 200ms ease, color 400ms ease',
}} }}
> >
{isHandRaised && !isScreenShare && ( {isHandRaised && !isScreenShare && (
<RiHand <>
color="black" <span>{positionInQueue}</span>
size={16} <RiHand
style={{ color="black"
marginRight: '0.4rem', size={16}
minWidth: '16px', style={{
animationDuration: '300ms', marginRight: '0.4rem',
animationName: 'wave_hand', marginLeft: '0.1rem',
animationIterationCount: '2', minWidth: '16px',
}} animationDuration: '300ms',
/> animationName: 'wave_hand',
animationIterationCount: '2',
}}
/>
</>
)} )}
{isScreenShare && ( {isScreenShare && (
<ScreenShareIcon <ScreenShareIcon

View File

@@ -1,11 +1,41 @@
import { LocalParticipant, Participant } from 'livekit-client' import { LocalParticipant, Participant } from 'livekit-client'
import { useParticipantAttribute } from '@livekit/components-react' import {
useParticipantAttribute,
useParticipants,
} from '@livekit/components-react'
import { isLocal } from '@/utils/livekit' import { isLocal } from '@/utils/livekit'
import { useMemo } from 'react'
type useRaisedHandProps = { type useRaisedHandProps = {
participant: Participant participant: Participant
} }
export function useRaisedHandPosition({ participant }: useRaisedHandProps) {
const { isHandRaised } = useRaisedHand({ participant })
const participants = useParticipants()
const positionInQueue = useMemo(() => {
if (!isHandRaised) return
return (
participants
.filter((p) => !!p.attributes.handRaisedAt)
.sort((a, b) => {
const dateA = new Date(a.attributes.handRaisedAt)
const dateB = new Date(b.attributes.handRaisedAt)
return dateA.getTime() - dateB.getTime()
})
.findIndex((p) => p.identity === participant.identity) + 1
)
}, [participants, participant, isHandRaised])
return {
positionInQueue,
firstInQueue: positionInQueue == 1,
}
}
export function useRaisedHand({ participant }: useRaisedHandProps) { export function useRaisedHand({ participant }: useRaisedHandProps) {
const handRaisedAtAttribute = useParticipantAttribute('handRaisedAt', { const handRaisedAtAttribute = useParticipantAttribute('handRaisedAt', {
participant, participant,