♻️(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 { useParticipants } from '@livekit/components-react'
import { Heading } from 'react-aria-components'
import { Box, Button, Div, H } from '@/primitives'
import { text } from '@/primitives/Text'
import { RiCloseLine } from '@remixicon/react'
import { participantsStore } from '@/stores/participants'
import { Div, H } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { allParticipantRoomEvents } from '@/features/rooms/livekit/constants/events'
import { ParticipantListItem } from '@/features/rooms/livekit/components/controls/Participants/ParticipantListItem'
import { ParticipantsCollapsableList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList'
import { HandRaisedListItem } from '@/features/rooms/livekit/components/controls/Participants/HandRaisedListItem'
import { LowerAllHandsButton } from '@/features/rooms/livekit/components/controls/Participants/LowerAllHandsButton'
import { ParticipantListItem } from '../../controls/Participants/ParticipantListItem'
import { ParticipantsCollapsableList } from '../../controls/Participants/ParticipantsCollapsableList'
import { HandRaisedListItem } from '../../controls/Participants/HandRaisedListItem'
import { LowerAllHandsButton } from '../../controls/Participants/LowerAllHandsButton'
// TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
export const ParticipantsList = () => {
@@ -44,76 +40,40 @@ export const ParticipantsList = () => {
// TODO - extract inline styling in a centralized styling file, and avoid magic numbers
return (
<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',
}}
<>
<H
lvl={2}
className={css({
fontSize: '0.875rem',
fontWeight: 'bold',
color: '#5f6368',
padding: '0 1.5rem',
marginBottom: '0.83em',
})}
>
{t('heading')}
</Heading>
<Div position="absolute" top="5" right="5">
<Button
invisible
size="xs"
onPress={() => (participantsStore.showParticipants = false)}
aria-label={t('closeButton')}
tooltip={t('closeButton')}
data-attr="participants-close"
>
<RiCloseLine />
</Button>
</Div>
<Div overflowY="scroll">
<H
lvl={2}
className={css({
fontSize: '0.875rem',
fontWeight: 'bold',
color: '#5f6368',
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>
{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} />
)}
<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 { useSnapshot } from 'valtio'
import { participantsStore } from '@/stores/participants.ts'
import { layoutStore } from '@/stores/layout'
export const useWidgetInteraction = () => {
const { dispatch, state } = useLayoutContext().widget
const participantsSnap = useSnapshot(participantsStore)
const isParticipantsOpen = participantsSnap.showParticipants
const layoutSnap = useSnapshot(layoutStore)
const sidePanel = layoutSnap.sidePanel
const isParticipantsOpen = sidePanel == 'participants'
const toggleParticipants = () => {
if (dispatch && state?.showChat) {
dispatch({ msg: 'toggle_chat' })
}
participantsStore.showParticipants = !isParticipantsOpen
layoutStore.sidePanel = isParticipantsOpen ? null : 'participants'
}
const toggleChat = () => {
if (isParticipantsOpen) {
participantsStore.showParticipants = false
layoutStore.sidePanel = null
}
if (dispatch) {
dispatch({ msg: 'toggle_chat' })

View File

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

View File

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

View File

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

View File

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

View File

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