♻️(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 { 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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' })
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -73,10 +73,17 @@
|
|||||||
"effects": ""
|
"effects": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sidePanel": {
|
||||||
|
"heading": {
|
||||||
|
"participants": ""
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"participants": ""
|
||||||
|
},
|
||||||
|
"closeButton": ""
|
||||||
|
},
|
||||||
"participants": {
|
"participants": {
|
||||||
"heading": "",
|
|
||||||
"subheading": "",
|
"subheading": "",
|
||||||
"closeButton": "",
|
|
||||||
"contributors": "",
|
"contributors": "",
|
||||||
"collapsable": {
|
"collapsable": {
|
||||||
"open": "",
|
"open": "",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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