♻️(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:
committed by
aleb_the_flash
parent
b9d13de591
commit
00fa7beebd
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -73,10 +73,17 @@
|
||||
"effects": ""
|
||||
}
|
||||
},
|
||||
"sidePanel": {
|
||||
"heading": {
|
||||
"participants": ""
|
||||
},
|
||||
"content": {
|
||||
"participants": ""
|
||||
},
|
||||
"closeButton": ""
|
||||
},
|
||||
"participants": {
|
||||
"heading": "",
|
||||
"subheading": "",
|
||||
"closeButton": "",
|
||||
"contributors": "",
|
||||
"collapsable": {
|
||||
"open": "",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -2,8 +2,10 @@ import { proxy } from 'valtio'
|
||||
|
||||
type State = {
|
||||
showHeader: boolean
|
||||
sidePanel: 'participants' | null
|
||||
}
|
||||
|
||||
export const layoutStore = proxy<State>({
|
||||
showHeader: false,
|
||||
sidePanel: null,
|
||||
})
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { proxy } from 'valtio'
|
||||
|
||||
type State = {
|
||||
showParticipants: boolean
|
||||
}
|
||||
|
||||
export const participantsStore = proxy<State>({
|
||||
showParticipants: false,
|
||||
})
|
||||
Reference in New Issue
Block a user