mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-21 07:36:43 +00:00
⚡️(frontend) reduce unnecessary re-renders in the participant panel
Cut down the number of unnecessary re-renders happening in the participant panel by narrowing subscriptions and isolating state to the components that actually depend on it.
This commit is contained in:
+1
-3
@@ -93,9 +93,7 @@ type ParticipantListItemProps = {
|
|||||||
participant: Participant
|
participant: Participant
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ParticipantListItem = ({
|
export const ParticipantRow = ({ participant }: ParticipantListItemProps) => {
|
||||||
participant,
|
|
||||||
}: ParticipantListItemProps) => {
|
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms')
|
||||||
const name = participant.name || participant.identity
|
const name = participant.name || participant.identity
|
||||||
return (
|
return (
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import { ReactNode, useState } from 'react'
|
|
||||||
import { css } from '@/styled-system/css'
|
|
||||||
import { ToggleButton } from 'react-aria-components'
|
|
||||||
import { HStack, styled, VStack } from '@/styled-system/jsx'
|
|
||||||
import { RiArrowUpSLine } from '@remixicon/react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
const ToggleHeader = styled(ToggleButton, {
|
|
||||||
base: {
|
|
||||||
minHeight: '40px', //fixme hardcoded value
|
|
||||||
paddingRight: '.5rem',
|
|
||||||
cursor: 'pointer',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
width: '100%',
|
|
||||||
alignItems: 'center',
|
|
||||||
transition: 'background 200ms',
|
|
||||||
borderTopRadius: '7px',
|
|
||||||
'&[data-hovered]': {
|
|
||||||
backgroundColor: 'greyscale.50',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const Container = styled('div', {
|
|
||||||
base: {
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'greyscale.250',
|
|
||||||
borderRadius: '8px',
|
|
||||||
margin: '0 .625rem',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const ListContainer = styled(VStack, {
|
|
||||||
base: {
|
|
||||||
borderTop: '1px solid',
|
|
||||||
borderTopColor: 'greyscale.250',
|
|
||||||
alignItems: 'start',
|
|
||||||
overflowY: 'scroll',
|
|
||||||
overflowX: 'hidden',
|
|
||||||
minHeight: 0,
|
|
||||||
flexGrow: 1,
|
|
||||||
display: 'flex',
|
|
||||||
paddingY: '0.5rem',
|
|
||||||
paddingX: '1rem',
|
|
||||||
gap: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export type ParticipantsCollapsableListProps<T> = {
|
|
||||||
heading: string
|
|
||||||
participants: Array<T>
|
|
||||||
renderParticipant: (participant: T) => JSX.Element
|
|
||||||
action?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ParticipantsCollapsableList<T>({
|
|
||||||
heading,
|
|
||||||
participants,
|
|
||||||
renderParticipant,
|
|
||||||
action,
|
|
||||||
}: ParticipantsCollapsableListProps<T>) {
|
|
||||||
const { t } = useTranslation('rooms')
|
|
||||||
const [isOpen, setIsOpen] = useState(true)
|
|
||||||
const label = t(`participants.collapsable.${isOpen ? 'close' : 'open'}`, {
|
|
||||||
name: heading,
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<ToggleHeader
|
|
||||||
isSelected={isOpen}
|
|
||||||
aria-label={label}
|
|
||||||
onPress={() => setIsOpen(!isOpen)}
|
|
||||||
style={{
|
|
||||||
borderRadius: !isOpen ? '7px' : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HStack
|
|
||||||
justify="space-between"
|
|
||||||
className={css({
|
|
||||||
margin: '0 1.25rem',
|
|
||||||
width: '100%',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
fontSize: '1rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{heading}
|
|
||||||
</div>
|
|
||||||
<div>{participants?.length || 0}</div>
|
|
||||||
</HStack>
|
|
||||||
<RiArrowUpSLine
|
|
||||||
size={32}
|
|
||||||
style={{
|
|
||||||
transform: isOpen ? 'rotate(-180deg)' : undefined,
|
|
||||||
transition: 'transform 200ms',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ToggleHeader>
|
|
||||||
{isOpen && (
|
|
||||||
<ListContainer>
|
|
||||||
{action}
|
|
||||||
{participants.map((participant) => renderParticipant(participant))}
|
|
||||||
</ListContainer>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { ReactNode, useId, useState } from 'react'
|
||||||
|
import { RiArrowUpSLine } from '@remixicon/react'
|
||||||
|
import { styled, HStack, VStack } from '@/styled-system/jsx'
|
||||||
|
|
||||||
|
const Container = styled('div', {
|
||||||
|
base: {
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'greyscale.250',
|
||||||
|
borderRadius: '8px',
|
||||||
|
margin: '0 .625rem 0.9375rem',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const Header = styled('button', {
|
||||||
|
base: {
|
||||||
|
minHeight: '2.5rem',
|
||||||
|
paddingX: '1.25rem',
|
||||||
|
paddingY: '0.5rem',
|
||||||
|
gap: '0.5rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%',
|
||||||
|
fontSize: '1rem',
|
||||||
|
transition: 'background 200ms',
|
||||||
|
borderTopRadius: '7px', // container radius (8) minus its 1px border
|
||||||
|
_hover: { backgroundColor: 'greyscale.50' },
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
isOpen: { false: { borderRadius: '7px' } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const Chevron = styled(RiArrowUpSLine, {
|
||||||
|
base: { transition: 'transform 200ms', flexShrink: 0 },
|
||||||
|
variants: {
|
||||||
|
isOpen: { false: { transform: 'rotate(180deg)' } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const List = styled(VStack, {
|
||||||
|
base: {
|
||||||
|
borderTop: '1px solid',
|
||||||
|
borderTopColor: 'greyscale.250',
|
||||||
|
alignItems: 'start',
|
||||||
|
minHeight: 0,
|
||||||
|
flexGrow: 1,
|
||||||
|
paddingY: '0.5rem',
|
||||||
|
paddingX: '1rem',
|
||||||
|
gap: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ParticipantsCollapsibleSectionProps = {
|
||||||
|
heading: string
|
||||||
|
count: number
|
||||||
|
action?: ReactNode
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ParticipantsCollapsibleSection = ({
|
||||||
|
heading,
|
||||||
|
count,
|
||||||
|
action,
|
||||||
|
children,
|
||||||
|
}: ParticipantsCollapsibleSectionProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(true)
|
||||||
|
const listId = useId()
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Header
|
||||||
|
type="button"
|
||||||
|
isOpen={isOpen}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-controls={listId}
|
||||||
|
onClick={() => setIsOpen((open) => !open)}
|
||||||
|
>
|
||||||
|
<HStack justify="space-between" width="100%">
|
||||||
|
<span>{heading}</span>
|
||||||
|
<span>{count}</span>
|
||||||
|
</HStack>
|
||||||
|
<Chevron size={32} isOpen={isOpen} aria-hidden />
|
||||||
|
</Header>
|
||||||
|
{isOpen && (
|
||||||
|
<List id={listId} role="list">
|
||||||
|
{action}
|
||||||
|
{children}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,54 +1,87 @@
|
|||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { useParticipants } from '@livekit/components-react'
|
import {
|
||||||
|
useRemoteParticipants,
|
||||||
|
useRoomContext,
|
||||||
|
} from '@livekit/components-react'
|
||||||
|
import { RoomEvent } from 'livekit-client'
|
||||||
import { Div, H } from '@/primitives'
|
import { Div, H } from '@/primitives'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ParticipantListItem } from './ParticipantListItem'
|
import { ParticipantRow } from './ParticipantRow'
|
||||||
import { ParticipantsCollapsableList } from './ParticipantsCollapsableList'
|
import { ParticipantsCollapsibleSection } from './ParticipantsCollapsibleSection'
|
||||||
import { HandRaisedListItem } from './HandRaisedListItem'
|
|
||||||
import { LowerAllHandsButton } from './LowerAllHandsButton'
|
import { LowerAllHandsButton } from './LowerAllHandsButton'
|
||||||
import { WaitingParticipantListItem } from './WaitingParticipantListItem'
|
|
||||||
import { useWaitingParticipants } from '@/features/participants/hooks/useWaitingParticipants'
|
|
||||||
import type { Participant } from 'livekit-client'
|
|
||||||
import { WaitingParticipant } from '@/features/participants/api/listWaitingParticipants'
|
|
||||||
import { MuteEveryoneButton } from './MuteEveryoneButton'
|
import { MuteEveryoneButton } from './MuteEveryoneButton'
|
||||||
|
import { WaitingParticipantsSection } from './WaitingParticipantsSection'
|
||||||
|
import { RaisedHandRow } from './RaisedHandRow'
|
||||||
|
|
||||||
|
const JoinedParticipantsSections = () => {
|
||||||
|
const room = useRoomContext()
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'participants' })
|
||||||
|
|
||||||
|
// `updateOnlyOn` filters on the room event *type*, not on which participant
|
||||||
|
// the event is about. `ParticipantNameChanged` and `ParticipantAttributesChanged`
|
||||||
|
// are room-wide and fire for the local participant too, so this hook re-emits
|
||||||
|
// (and we re-render) on local changes, despite its name.
|
||||||
|
//
|
||||||
|
// That is deliberate here: we prepend `room.localParticipant` to the list below,
|
||||||
|
// and its hand-raise state comes from `attributes.handRaisedAt`. Without the
|
||||||
|
// local events waking this component up, raising your own hand would not
|
||||||
|
// update the UI. Do not "optimise" these away.
|
||||||
|
const remoteParticipants = useRemoteParticipants({
|
||||||
|
updateOnlyOn: [
|
||||||
|
RoomEvent.ParticipantNameChanged,
|
||||||
|
RoomEvent.ParticipantAttributesChanged,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const allParticipants = [
|
||||||
|
room.localParticipant,
|
||||||
|
...[...remoteParticipants].sort((a, b) =>
|
||||||
|
(a.name || a.identity).localeCompare(b.name || b.identity)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
const participantsWithRaisedHands = allParticipants
|
||||||
|
.filter((p) => !!p.attributes.handRaisedAt)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const raisedAtA = Date.parse(a.attributes.handRaisedAt!)
|
||||||
|
const raisedAtB = Date.parse(b.attributes.handRaisedAt!)
|
||||||
|
return (
|
||||||
|
(Number.isNaN(raisedAtA) ? Infinity : raisedAtA) -
|
||||||
|
(Number.isNaN(raisedAtB) ? Infinity : raisedAtB)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{participantsWithRaisedHands.length > 0 && (
|
||||||
|
<ParticipantsCollapsibleSection
|
||||||
|
heading={t('raisedHands')}
|
||||||
|
count={participantsWithRaisedHands.length}
|
||||||
|
action={
|
||||||
|
<LowerAllHandsButton participants={participantsWithRaisedHands} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{participantsWithRaisedHands.map((p) => (
|
||||||
|
<RaisedHandRow key={p.identity} participant={p} />
|
||||||
|
))}
|
||||||
|
</ParticipantsCollapsibleSection>
|
||||||
|
)}
|
||||||
|
<ParticipantsCollapsibleSection
|
||||||
|
heading={t('contributors')}
|
||||||
|
count={allParticipants.length}
|
||||||
|
action={<MuteEveryoneButton participants={remoteParticipants} />}
|
||||||
|
>
|
||||||
|
{allParticipants.map((p) => (
|
||||||
|
<ParticipantRow key={p.identity} participant={p} />
|
||||||
|
))}
|
||||||
|
</ParticipantsCollapsibleSection>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
|
|
||||||
export const ParticipantsList = () => {
|
export const ParticipantsList = () => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'participants' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'participants' })
|
||||||
|
|
||||||
// Preferred using the 'useParticipants' hook rather than the separate remote and local hooks,
|
|
||||||
// because the 'useLocalParticipant' hook does not update the participant's information when their
|
|
||||||
// metadata/name changes. The LiveKit team has marked this as a TODO item in the code.
|
|
||||||
const participants = useParticipants()
|
|
||||||
|
|
||||||
const sortedRemoteParticipants = participants
|
|
||||||
.slice(1)
|
|
||||||
.sort((participantA, participantB) => {
|
|
||||||
const nameA = participantA.name || participantA.identity
|
|
||||||
const nameB = participantB.name || participantB.identity
|
|
||||||
return nameA.localeCompare(nameB)
|
|
||||||
})
|
|
||||||
|
|
||||||
const sortedParticipants = [
|
|
||||||
participants[0], // first participant returned by the hook, is always the local one
|
|
||||||
...sortedRemoteParticipants,
|
|
||||||
]
|
|
||||||
|
|
||||||
const raisedHandParticipants = participants
|
|
||||||
.filter((participant) => !!participant.attributes.handRaisedAt)
|
|
||||||
.sort((a, b) => {
|
|
||||||
const dateA = new Date(a.attributes.handRaisedAt)
|
|
||||||
const dateB = new Date(b.attributes.handRaisedAt)
|
|
||||||
const timeA = isNaN(dateA.getTime()) ? 0 : dateA.getTime()
|
|
||||||
const timeB = isNaN(dateB.getTime()) ? 0 : dateB.getTime()
|
|
||||||
return timeA - timeB
|
|
||||||
})
|
|
||||||
|
|
||||||
const { waitingParticipants, handleParticipantEntry } =
|
|
||||||
useWaitingParticipants()
|
|
||||||
|
|
||||||
// TODO - extract inline styling in a centralized styling file, and avoid magic numbers
|
|
||||||
return (
|
return (
|
||||||
<Div overflowY="scroll">
|
<Div overflowY="scroll">
|
||||||
<H
|
<H
|
||||||
@@ -63,50 +96,8 @@ export const ParticipantsList = () => {
|
|||||||
>
|
>
|
||||||
{t('subheading').toUpperCase()}
|
{t('subheading').toUpperCase()}
|
||||||
</H>
|
</H>
|
||||||
{waitingParticipants?.length > 0 && (
|
<WaitingParticipantsSection />
|
||||||
<Div marginBottom=".9375rem">
|
<JoinedParticipantsSections />
|
||||||
<ParticipantsCollapsableList<WaitingParticipant>
|
|
||||||
heading={t('waiting.title')}
|
|
||||||
participants={waitingParticipants}
|
|
||||||
renderParticipant={(participant) => (
|
|
||||||
<WaitingParticipantListItem
|
|
||||||
key={participant.id}
|
|
||||||
participant={participant}
|
|
||||||
onAction={handleParticipantEntry}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
action={<></>}
|
|
||||||
/>
|
|
||||||
</Div>
|
|
||||||
)}
|
|
||||||
{raisedHandParticipants.length > 0 && (
|
|
||||||
<Div marginBottom=".9375rem">
|
|
||||||
<ParticipantsCollapsableList<Participant>
|
|
||||||
heading={t('raisedHands')}
|
|
||||||
participants={raisedHandParticipants}
|
|
||||||
renderParticipant={(participant) => (
|
|
||||||
<HandRaisedListItem
|
|
||||||
key={participant.identity}
|
|
||||||
participant={participant}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
action={
|
|
||||||
<LowerAllHandsButton participants={raisedHandParticipants} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Div>
|
|
||||||
)}
|
|
||||||
<ParticipantsCollapsableList<Participant>
|
|
||||||
heading={t('contributors')}
|
|
||||||
participants={sortedParticipants}
|
|
||||||
renderParticipant={(participant) => (
|
|
||||||
<ParticipantListItem
|
|
||||||
key={participant.identity}
|
|
||||||
participant={participant}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
action={<MuteEveryoneButton participants={sortedRemoteParticipants} />}
|
|
||||||
/>
|
|
||||||
</Div>
|
</Div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-20
@@ -6,25 +6,45 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { Avatar } from '@/components/Avatar'
|
import { Avatar } from '@/components/Avatar'
|
||||||
import { useLowerHandParticipant } from '../api/lowerHandParticipant'
|
import { useLowerHandParticipant } from '../api/lowerHandParticipant'
|
||||||
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
|
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
|
||||||
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
|
|
||||||
import type { Participant } from 'livekit-client'
|
import type { Participant } from 'livekit-client'
|
||||||
import { isLocal } from '@/utils/livekit'
|
import { isLocal } from '@/utils/livekit'
|
||||||
import { RiHand } from '@remixicon/react'
|
import { RiHand } from '@remixicon/react'
|
||||||
import { Button } from '@/primitives'
|
import { Button } from '@/primitives'
|
||||||
|
import { AdminOrOwnerOnly } from '@/features/rooms/components/AdminOrOwnerOnly'
|
||||||
|
|
||||||
|
const ActionButton = ({
|
||||||
|
participant,
|
||||||
|
name,
|
||||||
|
}: {
|
||||||
|
participant: Participant
|
||||||
|
name: string
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation('rooms')
|
||||||
|
const { lowerHandParticipant } = useLowerHandParticipant()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
square
|
||||||
|
variant="greyscale"
|
||||||
|
size="sm"
|
||||||
|
onPress={() => lowerHandParticipant(participant)}
|
||||||
|
aria-label={t('participants.lowerParticipantHand', { name })}
|
||||||
|
tooltip={t('participants.lowerParticipantHand', { name })}
|
||||||
|
data-attr="participants-lower-hand"
|
||||||
|
>
|
||||||
|
<RiHand />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
type HandRaisedListItemProps = {
|
type HandRaisedListItemProps = {
|
||||||
participant: Participant
|
participant: Participant
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HandRaisedListItem = ({
|
export const RaisedHandRow = ({ participant }: HandRaisedListItemProps) => {
|
||||||
participant,
|
|
||||||
}: HandRaisedListItemProps) => {
|
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms')
|
||||||
const name = participant.name || participant.identity
|
const name = participant.name || participant.identity
|
||||||
|
|
||||||
const { lowerHandParticipant } = useLowerHandParticipant()
|
|
||||||
const isAdminOrOwner = useIsAdminOrOwner()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
role="listitem"
|
role="listitem"
|
||||||
@@ -69,19 +89,9 @@ export const HandRaisedListItem = ({
|
|||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
{isAdminOrOwner && (
|
<AdminOrOwnerOnly>
|
||||||
<Button
|
<ActionButton participant={participant} name={name} />
|
||||||
square
|
</AdminOrOwnerOnly>
|
||||||
variant="greyscale"
|
|
||||||
size="sm"
|
|
||||||
onPress={() => lowerHandParticipant(participant)}
|
|
||||||
aria-label={t('participants.lowerParticipantHand', { name })}
|
|
||||||
tooltip={t('participants.lowerParticipantHand', { name })}
|
|
||||||
data-attr="participants-lower-hand"
|
|
||||||
>
|
|
||||||
<RiHand />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</HStack>
|
</HStack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+1
-1
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { WaitingParticipant } from '../api/listWaitingParticipants'
|
import { WaitingParticipant } from '../api/listWaitingParticipants'
|
||||||
import { RiCloseLine } from '@remixicon/react'
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
|
|
||||||
export const WaitingParticipantListItem = ({
|
export const WaitingParticipantRow = ({
|
||||||
participant,
|
participant,
|
||||||
onAction,
|
onAction,
|
||||||
}: {
|
}: {
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { ParticipantsCollapsibleSection } from './ParticipantsCollapsibleSection'
|
||||||
|
import { WaitingParticipantRow } from './WaitingParticipantRow'
|
||||||
|
import { useWaitingParticipants } from '../hooks/useWaitingParticipants'
|
||||||
|
|
||||||
|
export const WaitingParticipantsSection = () => {
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'participants.waiting' })
|
||||||
|
|
||||||
|
const { waitingParticipants, handleParticipantEntry } =
|
||||||
|
useWaitingParticipants()
|
||||||
|
|
||||||
|
if (waitingParticipants?.length == 0) return
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ParticipantsCollapsibleSection
|
||||||
|
heading={t('title')}
|
||||||
|
count={waitingParticipants.length}
|
||||||
|
>
|
||||||
|
{waitingParticipants.map((p) => (
|
||||||
|
<WaitingParticipantRow
|
||||||
|
key={p.id}
|
||||||
|
participant={p}
|
||||||
|
onAction={handleParticipantEntry}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ParticipantsCollapsibleSection>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user