🚸(frontend) introduce sections in participants menu
Why? This layout is extensible. This menu will have two sections, to separate in-room participants from the waiting room ones. It's copied from Gmeet User Experience. In-room participants will be divided in sub-lists based on their states (ex: hand raised, …). This User Experience is extensible if in the future with support subroom in a room or whatever.
This commit is contained in:
committed by
aleb_the_flash
parent
20464a2845
commit
1b2e0ad431
@@ -0,0 +1,106 @@
|
|||||||
|
import { 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 { Participant } from 'livekit-client'
|
||||||
|
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: '#f5f5f5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const Container = styled('div', {
|
||||||
|
base: {
|
||||||
|
border: '1px solid #dadce0',
|
||||||
|
borderRadius: '8px',
|
||||||
|
margin: '0 .625rem',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const ListContainer = styled(VStack, {
|
||||||
|
base: {
|
||||||
|
borderTop: '1px solid #dadce0',
|
||||||
|
alignItems: 'start',
|
||||||
|
overflowY: 'scroll',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
minHeight: 0,
|
||||||
|
flexGrow: 1,
|
||||||
|
display: 'flex',
|
||||||
|
paddingY: '0.5rem',
|
||||||
|
paddingX: '1rem',
|
||||||
|
gap: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type ParticipantsCollapsableListProps = {
|
||||||
|
heading: string
|
||||||
|
participants: Array<Participant>
|
||||||
|
renderParticipant: (participant: Participant) => JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ParticipantsCollapsableList = ({
|
||||||
|
heading,
|
||||||
|
participants,
|
||||||
|
renderParticipant,
|
||||||
|
}: ParticipantsCollapsableListProps) => {
|
||||||
|
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>
|
||||||
|
{participants.map((participant) => renderParticipant(participant))}
|
||||||
|
</ListContainer>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,14 +2,14 @@ import { css } from '@/styled-system/css'
|
|||||||
import { useParticipants } from '@livekit/components-react'
|
import { useParticipants } from '@livekit/components-react'
|
||||||
|
|
||||||
import { Heading } from 'react-aria-components'
|
import { Heading } from 'react-aria-components'
|
||||||
import { Box, Button, Div } from '@/primitives'
|
import { Box, Button, Div, H } from '@/primitives'
|
||||||
import { text } from '@/primitives/Text'
|
import { text } from '@/primitives/Text'
|
||||||
import { RiCloseLine } from '@remixicon/react'
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
import { participantsStore } from '@/stores/participants'
|
import { participantsStore } from '@/stores/participants'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { allParticipantRoomEvents } from '@/features/rooms/livekit/constants/events'
|
import { allParticipantRoomEvents } from '@/features/rooms/livekit/constants/events'
|
||||||
import { ParticipantListItem } from '@/features/rooms/livekit/components/controls/Participants/ParticipantListItem'
|
import { ParticipantListItem } from '@/features/rooms/livekit/components/controls/Participants/ParticipantListItem'
|
||||||
import { VStack } from '@/styled-system/jsx'
|
import { ParticipantsCollapsableList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList'
|
||||||
|
|
||||||
// TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
|
// TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
|
||||||
export const ParticipantsList = () => {
|
export const ParticipantsList = () => {
|
||||||
@@ -39,25 +39,26 @@ export const ParticipantsList = () => {
|
|||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
size="sm"
|
size="sm"
|
||||||
minWidth="300px"
|
minWidth="360px"
|
||||||
className={css({
|
className={css({
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
margin: '1.5rem 1.5rem 1.5rem 0',
|
margin: '1.5rem 1.5rem 1.5rem 0',
|
||||||
|
padding: 0,
|
||||||
|
gap: 0,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Heading slot="title" level={3} className={text({ variant: 'h2' })}>
|
<Heading
|
||||||
<span>{t('participants.heading')}</span>{' '}
|
slot="title"
|
||||||
<span
|
level={1}
|
||||||
className={css({
|
className={text({ variant: 'h2' })}
|
||||||
marginLeft: '0.75rem',
|
style={{
|
||||||
fontWeight: 'normal',
|
paddingLeft: '1.5rem',
|
||||||
fontSize: '1rem',
|
paddingTop: '1rem',
|
||||||
})}
|
}}
|
||||||
>
|
>
|
||||||
{participants?.length}
|
{t('participants.heading')}
|
||||||
</span>
|
|
||||||
</Heading>
|
</Heading>
|
||||||
<Div position="absolute" top="5" right="5">
|
<Div position="absolute" top="5" right="5">
|
||||||
<Button
|
<Button
|
||||||
@@ -70,24 +71,25 @@ export const ParticipantsList = () => {
|
|||||||
<RiCloseLine />
|
<RiCloseLine />
|
||||||
</Button>
|
</Button>
|
||||||
</Div>
|
</Div>
|
||||||
{sortedParticipants?.length > 0 && (
|
<H
|
||||||
<VStack
|
lvl={2}
|
||||||
role="list"
|
className={css({
|
||||||
className={css({
|
fontSize: '0.875rem',
|
||||||
alignItems: 'start',
|
fontWeight: 'bold',
|
||||||
gap: 'none',
|
color: '#5f6368',
|
||||||
overflowY: 'scroll',
|
padding: '0 1.5rem',
|
||||||
overflowX: 'hidden',
|
marginBottom: '0.83em',
|
||||||
minHeight: 0,
|
})}
|
||||||
flexGrow: 1,
|
>
|
||||||
display: 'flex',
|
{t('participants.subheading').toUpperCase()}
|
||||||
})}
|
</H>
|
||||||
>
|
<ParticipantsCollapsableList
|
||||||
{sortedParticipants.map((participant) => (
|
heading={t('participants.contributors')}
|
||||||
<ParticipantListItem participant={participant} />
|
participants={sortedParticipants}
|
||||||
))}
|
renderParticipant={(participant) => (
|
||||||
</VStack>
|
<ParticipantListItem participant={participant} />
|
||||||
)}
|
)}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,13 @@
|
|||||||
},
|
},
|
||||||
"participants": {
|
"participants": {
|
||||||
"heading": "",
|
"heading": "",
|
||||||
|
"subheading": "",
|
||||||
"closeButton": "",
|
"closeButton": "",
|
||||||
|
"contributors": "",
|
||||||
|
"collapsable": {
|
||||||
|
"open": "",
|
||||||
|
"close": ""
|
||||||
|
},
|
||||||
"you": "",
|
"you": "",
|
||||||
"muteParticipant": "",
|
"muteParticipant": "",
|
||||||
"muteParticipantAlert": {
|
"muteParticipantAlert": {
|
||||||
|
|||||||
@@ -53,8 +53,14 @@
|
|||||||
},
|
},
|
||||||
"participants": {
|
"participants": {
|
||||||
"heading": "Participants",
|
"heading": "Participants",
|
||||||
|
"subheading": "In room",
|
||||||
"closeButton": "Hide participants",
|
"closeButton": "Hide participants",
|
||||||
"you": "You",
|
"you": "You",
|
||||||
|
"contributors": "Contributors",
|
||||||
|
"collapsable": {
|
||||||
|
"open": "Open {{name}} list",
|
||||||
|
"close": "Close {{name}} list"
|
||||||
|
},
|
||||||
"muteParticipant": "Close the mic of {{name}}",
|
"muteParticipant": "Close the mic of {{name}}",
|
||||||
"muteParticipantAlert": {
|
"muteParticipantAlert": {
|
||||||
"description": "Mute {{name}} for all participants? {{name}} is the only person who can unmute themselves.",
|
"description": "Mute {{name}} for all participants? {{name}} is the only person who can unmute themselves.",
|
||||||
|
|||||||
@@ -53,8 +53,14 @@
|
|||||||
},
|
},
|
||||||
"participants": {
|
"participants": {
|
||||||
"heading": "Participants",
|
"heading": "Participants",
|
||||||
|
"subheading": "Dans la réunion",
|
||||||
"closeButton": "Masquer les participants",
|
"closeButton": "Masquer les participants",
|
||||||
"you": "Vous",
|
"you": "Vous",
|
||||||
|
"contributors": "Contributeurs",
|
||||||
|
"collapsable": {
|
||||||
|
"open": "Ouvrir la liste {{name}}",
|
||||||
|
"close": "Fermer la liste {{name}}"
|
||||||
|
},
|
||||||
"muteParticipant": "Couper le micro de {{name}}",
|
"muteParticipant": "Couper le micro de {{name}}",
|
||||||
"muteParticipantAlert": {
|
"muteParticipantAlert": {
|
||||||
"description": "Couper le micro de {{name}} pour tous les participants ? {{name}} est la seule personne habilitée à réactiver son micro",
|
"description": "Couper le micro de {{name}} pour tous les participants ? {{name}} est la seule personne habilitée à réactiver son micro",
|
||||||
|
|||||||
Reference in New Issue
Block a user