♻️(frontend) introduce a side panel

Refactor side panel into a reusable component to display any interactive
content like menus, messages, participant lists, or effects. Establish it
as a core feature of the videoconference tool.

Improve extensibility and better share responsibilities. The next step is to
refactor the chat to render inside the side panel.
This commit is contained in:
lebaudantoine
2024-09-18 16:28:31 +02:00
committed by aleb_the_flash
parent b9d13de591
commit 00fa7beebd
9 changed files with 165 additions and 103 deletions

View File

@@ -0,0 +1,86 @@
import { useSnapshot } from 'valtio'
import { layoutStore } from '@/stores/layout'
import { css } from '@/styled-system/css'
import { Heading } from 'react-aria-components'
import { text } from '@/primitives/Text'
import { Box, Button, Div } from '@/primitives'
import { RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList'
import { useWidgetInteraction } from '../hooks/useWidgetInteraction'
import { ReactNode } from 'react'
type StyledSidePanelProps = {
title: string
children: ReactNode
onClose: () => void
closeButtonTooltip: string
}
const StyledSidePanel = ({
title,
children,
onClose,
closeButtonTooltip,
}: StyledSidePanelProps) => (
<Box
size="sm"
minWidth="360px"
className={css({
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
margin: '1.5rem 1.5rem 1.5rem 0',
padding: 0,
gap: 0,
})}
>
<Heading
slot="title"
level={1}
className={text({ variant: 'h2' })}
style={{
paddingLeft: '1.5rem',
paddingTop: '1rem',
}}
>
{title}
</Heading>
<Div position="absolute" top="5" right="5">
<Button
invisible
size="xs"
onPress={onClose}
aria-label={closeButtonTooltip}
tooltip={closeButtonTooltip}
>
<RiCloseLine />
</Button>
</Div>
<Div overflowY="scroll">{children}</Div>
</Box>
)
export const SidePanel = () => {
const layoutSnap = useSnapshot(layoutStore)
const sidePanel = layoutSnap.sidePanel
const { isParticipantsOpen } = useWidgetInteraction()
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
if (!sidePanel) {
return
}
return (
<StyledSidePanel
title={t(`heading.${sidePanel}`)}
onClose={() => (layoutStore.sidePanel = null)}
closeButtonTooltip={t('closeButton', {
content: t(`content.${sidePanel}`),
})}
>
{isParticipantsOpen && <ParticipantsList />}
</StyledSidePanel>
)
}

View File

@@ -1,17 +1,13 @@
import { css } from '@/styled-system/css' 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 { Div, H } from '@/primitives'
import { Box, Button, Div, H } from '@/primitives'
import { text } from '@/primitives/Text'
import { RiCloseLine } from '@remixicon/react'
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 '../../controls/Participants/ParticipantListItem'
import { ParticipantsCollapsableList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList' import { ParticipantsCollapsableList } from '../../controls/Participants/ParticipantsCollapsableList'
import { HandRaisedListItem } from '@/features/rooms/livekit/components/controls/Participants/HandRaisedListItem' import { HandRaisedListItem } from '../../controls/Participants/HandRaisedListItem'
import { LowerAllHandsButton } from '@/features/rooms/livekit/components/controls/Participants/LowerAllHandsButton' import { LowerAllHandsButton } from '../../controls/Participants/LowerAllHandsButton'
// 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 = () => {
@@ -44,76 +40,40 @@ export const ParticipantsList = () => {
// TODO - extract inline styling in a centralized styling file, and avoid magic numbers // TODO - extract inline styling in a centralized styling file, and avoid magic numbers
return ( return (
<Box <>
size="sm" <H
minWidth="360px" lvl={2}
className={css({ className={css({
overflow: 'hidden', fontSize: '0.875rem',
display: 'flex', fontWeight: 'bold',
flexDirection: 'column', color: '#5f6368',
margin: '1.5rem 1.5rem 1.5rem 0', padding: '0 1.5rem',
padding: 0, marginBottom: '0.83em',
gap: 0, })}
})}
>
<Heading
slot="title"
level={1}
className={text({ variant: 'h2' })}
style={{
paddingLeft: '1.5rem',
paddingTop: '1rem',
}}
> >
{t('heading')} {t('subheading').toUpperCase()}
</Heading> </H>
<Div position="absolute" top="5" right="5"> {raisedHandParticipants.length > 0 && (
<Button <Div marginBottom=".9375rem">
invisible <ParticipantsCollapsableList
size="xs" heading={t('raisedHands')}
onPress={() => (participantsStore.showParticipants = false)} participants={raisedHandParticipants}
aria-label={t('closeButton')} renderParticipant={(participant) => (
tooltip={t('closeButton')} <HandRaisedListItem participant={participant} />
data-attr="participants-close" )}
> action={() => (
<RiCloseLine /> <LowerAllHandsButton participants={raisedHandParticipants} />
</Button> )}
</Div> />
<Div overflowY="scroll"> </Div>
<H )}
lvl={2} <ParticipantsCollapsableList
className={css({ heading={t('contributors')}
fontSize: '0.875rem', participants={sortedParticipants}
fontWeight: 'bold', renderParticipant={(participant) => (
color: '#5f6368', <ParticipantListItem participant={participant} />
padding: '0 1.5rem',
marginBottom: '0.83em',
})}
>
{t('subheading').toUpperCase()}
</H>
{raisedHandParticipants.length > 0 && (
<Div marginBottom=".9375rem">
<ParticipantsCollapsableList
heading={t('raisedHands')}
participants={raisedHandParticipants}
renderParticipant={(participant) => (
<HandRaisedListItem participant={participant} />
)}
action={() => (
<LowerAllHandsButton participants={raisedHandParticipants} />
)}
/>
</Div>
)} )}
<ParticipantsCollapsableList />
heading={t('contributors')} </>
participants={sortedParticipants}
renderParticipant={(participant) => (
<ParticipantListItem participant={participant} />
)}
/>
</Div>
</Box>
) )
} }

View File

@@ -1,23 +1,25 @@
import { useLayoutContext } from '@livekit/components-react' import { useLayoutContext } from '@livekit/components-react'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { participantsStore } from '@/stores/participants.ts' import { layoutStore } from '@/stores/layout'
export const useWidgetInteraction = () => { export const useWidgetInteraction = () => {
const { dispatch, state } = useLayoutContext().widget const { dispatch, state } = useLayoutContext().widget
const participantsSnap = useSnapshot(participantsStore) const layoutSnap = useSnapshot(layoutStore)
const isParticipantsOpen = participantsSnap.showParticipants const sidePanel = layoutSnap.sidePanel
const isParticipantsOpen = sidePanel == 'participants'
const toggleParticipants = () => { const toggleParticipants = () => {
if (dispatch && state?.showChat) { if (dispatch && state?.showChat) {
dispatch({ msg: 'toggle_chat' }) dispatch({ msg: 'toggle_chat' })
} }
participantsStore.showParticipants = !isParticipantsOpen layoutStore.sidePanel = isParticipantsOpen ? null : 'participants'
} }
const toggleChat = () => { const toggleChat = () => {
if (isParticipantsOpen) { if (isParticipantsOpen) {
participantsStore.showParticipants = false layoutStore.sidePanel = null
} }
if (dispatch) { if (dispatch) {
dispatch({ msg: 'toggle_chat' }) dispatch({ msg: 'toggle_chat' })

View File

@@ -30,11 +30,11 @@ import {
import { ControlBar } from './ControlBar' import { ControlBar } from './ControlBar'
import { styled } from '@/styled-system/jsx' import { styled } from '@/styled-system/jsx'
import { cva } from '@/styled-system/css' import { cva } from '@/styled-system/css'
import { ParticipantsList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsList'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { participantsStore } from '@/stores/participants' import { layoutStore } from '@/stores/layout'
import { FocusLayout } from '../components/FocusLayout' import { FocusLayout } from '../components/FocusLayout'
import { ParticipantTile } from '../components/ParticipantTile' import { ParticipantTile } from '../components/ParticipantTile'
import { SidePanel } from '../components/SidePanel'
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast' import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
const LayoutWrapper = styled( const LayoutWrapper = styled(
@@ -172,8 +172,8 @@ export function VideoConference({
]) ])
/* eslint-enable react-hooks/exhaustive-deps */ /* eslint-enable react-hooks/exhaustive-deps */
const participantsSnap = useSnapshot(participantsStore) const layoutSnap = useSnapshot(layoutStore)
const showParticipants = participantsSnap.showParticipants const sidePanel = layoutSnap.sidePanel
return ( return (
<div className="lk-video-conference" {...props}> <div className="lk-video-conference" {...props}>
@@ -223,7 +223,7 @@ export function VideoConference({
messageEncoder={chatMessageEncoder} messageEncoder={chatMessageEncoder}
messageDecoder={chatMessageDecoder} messageDecoder={chatMessageDecoder}
/> />
{showParticipants && <ParticipantsList />} {sidePanel && <SidePanel />}
</LayoutWrapper> </LayoutWrapper>
<ControlBar /> <ControlBar />
</div> </div>

View File

@@ -73,10 +73,17 @@
"effects": "" "effects": ""
} }
}, },
"sidePanel": {
"heading": {
"participants": ""
},
"content": {
"participants": ""
},
"closeButton": ""
},
"participants": { "participants": {
"heading": "",
"subheading": "", "subheading": "",
"closeButton": "",
"contributors": "", "contributors": "",
"collapsable": { "collapsable": {
"open": "", "open": "",

View File

@@ -71,10 +71,17 @@
"effects": "Apply effects" "effects": "Apply effects"
} }
}, },
"sidePanel": {
"heading": {
"participants": "Participants"
},
"content": {
"participants": "participants"
},
"closeButton": "Hide {{content}}"
},
"participants": { "participants": {
"heading": "Participants",
"subheading": "In room", "subheading": "In room",
"closeButton": "Hide participants",
"you": "You", "you": "You",
"contributors": "Contributors", "contributors": "Contributors",
"collapsable": { "collapsable": {

View File

@@ -71,10 +71,17 @@
"effects": "Appliquer des effets" "effects": "Appliquer des effets"
} }
}, },
"sidePanel": {
"heading": {
"participants": "Participants"
},
"content": {
"participants": "les participants"
},
"closeButton": "Masquer {{content}}"
},
"participants": { "participants": {
"heading": "Participants",
"subheading": "Dans la réunion", "subheading": "Dans la réunion",
"closeButton": "Masquer les participants",
"you": "Vous", "you": "Vous",
"contributors": "Contributeurs", "contributors": "Contributeurs",
"collapsable": { "collapsable": {

View File

@@ -2,8 +2,10 @@ import { proxy } from 'valtio'
type State = { type State = {
showHeader: boolean showHeader: boolean
sidePanel: 'participants' | null
} }
export const layoutStore = proxy<State>({ export const layoutStore = proxy<State>({
showHeader: false, showHeader: false,
sidePanel: null,
}) })

View File

@@ -1,9 +0,0 @@
import { proxy } from 'valtio'
type State = {
showParticipants: boolean
}
export const participantsStore = proxy<State>({
showParticipants: false,
})