️(frontend) encapsulate participant metadata in a boundary component

Encapsulate the participant metadata rendering in a clear boundary
component, especially by pushing the raised-hand logic down into a
dedicated child.

This way, when a participant raises their hand, only the child
component re-renders instead of the whole ParticipantTile.
This commit is contained in:
lebaudantoine
2026-07-15 12:49:56 +02:00
committed by aleb_the_flash
parent 4598aafa23
commit 9420fed563
2 changed files with 100 additions and 93 deletions
@@ -0,0 +1,95 @@
import {
useRaisedHand,
useRaisedHandPosition,
} from '@/features/rooms/livekit/hooks/useRaisedHand'
import {
ConnectionQualityIndicator,
LockLockedIcon,
ScreenShareIcon,
useIsEncrypted,
useParticipantInfo,
} from '@livekit/components-react'
import { HStack } from '@/styled-system/jsx'
import { Participant } from 'livekit-client'
import { MutedMicIndicator } from './MutedMicIndicator'
import { RiHand } from '@remixicon/react'
import { ParticipantName } from './ParticipantName'
export const ParticipantMetadata = ({
participant,
isScreenShare,
}: {
participant: Participant
isScreenShare: boolean
}) => {
const { identity, name } = useParticipantInfo({ participant })
const isEncrypted = useIsEncrypted(participant)
const { isHandRaised } = useRaisedHand({ participant })
const { positionInQueue, firstInQueue } = useRaisedHandPosition({
participant,
})
return (
<div className="lk-participant-metadata">
<HStack gap={0.25}>
{!isScreenShare && <MutedMicIndicator participant={participant} />}
<div
className="lk-participant-metadata-item"
style={{
padding: '0.1rem 0.25rem',
backgroundColor:
isHandRaised && !isScreenShare
? firstInQueue
? '#fde047'
: 'white'
: undefined,
color: isHandRaised && !isScreenShare ? 'black' : undefined,
transition: 'background 200ms ease, color 400ms ease',
}}
>
{isHandRaised && !isScreenShare && (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.1rem',
}}
>
<span>{positionInQueue}</span>
<RiHand
color="black"
size={16}
style={{
marginRight: '0.4rem',
marginLeft: '0.1rem',
minWidth: '16px',
animationDuration: '300ms',
animationName: 'wave_hand',
animationIterationCount: '2',
}}
/>
</span>
)}
{isScreenShare && (
<ScreenShareIcon
style={{
maxWidth: '20px',
width: '100%',
}}
/>
)}
{isEncrypted && !isScreenShare && (
<LockLockedIcon style={{ marginRight: '0.25rem' }} />
)}
<div className="lk-participant-name-wrapper">
<ParticipantName
displayedName={name != '' ? name : identity}
isScreenShare={isScreenShare}
/>
</div>
</div>
</HStack>
<ConnectionQualityIndicator className="lk-participant-metadata-item" />
</div>
)
}
@@ -1,18 +1,13 @@
import {
AudioTrack,
ConnectionQualityIndicator,
LockLockedIcon,
ParticipantTileProps,
ScreenShareIcon,
useEnsureTrackRef,
useFeatureContext,
useIsEncrypted,
useMaybeTrackRefContext,
useParticipantTile,
VideoTrack,
TrackRefContext,
ParticipantContextIfNeeded,
useParticipantInfo,
} from '@livekit/components-react'
import React from 'react'
import {
@@ -21,23 +16,16 @@ import {
TrackReferenceOrPlaceholder,
} from '@livekit/components-core'
import { Track } from 'livekit-client'
import { RiHand } from '@remixicon/react'
import {
useRaisedHand,
useRaisedHandPosition,
} from '@/features/rooms/livekit/hooks/useRaisedHand'
import { HStack } from '@/styled-system/jsx'
import { MutedMicIndicator } from './MutedMicIndicator'
import { ParticipantPlaceholder } from './ParticipantPlaceholder'
import { ParticipantTileFocus } from './ParticipantTileFocus'
import { FullScreenShareWarning } from './FullScreenShareWarning'
import { ParticipantName } from './ParticipantName'
import { getParticipantName } from '@/features/rooms/utils/getParticipantName'
import { useTranslation } from 'react-i18next'
import { getShortcutDescriptorById } from '@/features/shortcuts/catalog'
import { formatShortcutLabel } from '@/features/shortcuts/formatLabels'
import { KeyboardShortcutHint } from './KeyboardShortcutHint'
import { layoutStore, clearPinnedTrack } from '@/stores/layout'
import { ParticipantMetadata } from './ParticipantMetadata'
export function TrackRefContextIfNeeded(
props: React.PropsWithChildren<{
@@ -77,18 +65,12 @@ export const ParticipantTile: (
ref
) {
const trackReference = useEnsureTrackRef(trackRef)
const { identity, name } = useParticipantInfo({
participant: trackReference.participant,
})
const { elementProps } = useParticipantTile<HTMLDivElement>({
htmlProps,
disableSpeakingIndicator,
onParticipantClick,
trackRef: trackReference,
})
const isEncrypted = useIsEncrypted(trackReference.participant)
const autoManageSubscription = useFeatureContext()?.autoSubscription
const handleSubscribe = React.useCallback(
@@ -105,14 +87,6 @@ export const ParticipantTile: (
[trackReference]
)
const { isHandRaised } = useRaisedHand({
participant: trackReference.participant,
})
const { positionInQueue, firstInQueue } = useRaisedHandPosition({
participant: trackReference.participant,
})
const isScreenShare = trackReference.source != Track.Source.Camera
const [hasKeyboardFocus, setHasKeyboardFocus] = React.useState(false)
@@ -169,72 +143,10 @@ export const ParticipantTile: (
/>
</div>
{!disableMetadata && (
<div className="lk-participant-metadata">
<HStack gap={0.25}>
{!isScreenShare && (
<MutedMicIndicator
participant={trackReference.participant}
/>
)}
<div
className="lk-participant-metadata-item"
style={{
padding: '0.1rem 0.25rem',
backgroundColor:
isHandRaised && !isScreenShare
? firstInQueue
? '#fde047'
: 'white'
: undefined,
color:
isHandRaised && !isScreenShare ? 'black' : undefined,
transition: 'background 200ms ease, color 400ms ease',
}}
>
{isHandRaised && !isScreenShare && (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.1rem',
}}
>
<span>{positionInQueue}</span>
<RiHand
color="black"
size={16}
style={{
marginRight: '0.4rem',
marginLeft: '0.1rem',
minWidth: '16px',
animationDuration: '300ms',
animationName: 'wave_hand',
animationIterationCount: '2',
}}
/>
</span>
)}
{isScreenShare && (
<ScreenShareIcon
style={{
maxWidth: '20px',
width: '100%',
}}
/>
)}
{isEncrypted && !isScreenShare && (
<LockLockedIcon style={{ marginRight: '0.25rem' }} />
)}
<div className="lk-participant-name-wrapper">
<ParticipantName
displayedName={name != '' ? name : identity}
isScreenShare={isScreenShare}
/>
</div>
</div>
</HStack>
<ConnectionQualityIndicator className="lk-participant-metadata-item" />
</div>
<ParticipantMetadata
isScreenShare={isScreenShare}
participant={trackReference.participant}
/>
)}
</>
)}