mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-11 03:07:43 +00:00
64b4200fc3
- Dissociate the chat observer (which persists messages in a store) from chat rendering. - Do not keep the chat mounted in the DOM anymore. - Recompute the minimal amount of data when a participant renames. - Memoize most of the layout. - Drop the manual textarea row-size computation: recent React Aria already handles it. Known regressions still to solve: focus behavior on the input, scroll behavior on message updates, and edge cases around list sizing. Not sure yet whether there is a better way to memoize the virtualized list; open to feedback.
236 lines
6.2 KiB
TypeScript
236 lines
6.2 KiB
TypeScript
import { layoutStore } from '@/stores/layout'
|
|
import { css } from '@/styled-system/css'
|
|
import { Heading } from 'react-aria-components'
|
|
import { text } from '@/primitives/Text'
|
|
import { Button, Div } from '@/primitives'
|
|
import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { ParticipantsList } from './controls/Participants/ParticipantsList'
|
|
import { PanelId, useSidePanel } from '../hooks/useSidePanel'
|
|
import React, { ReactNode, useCallback, useRef } from 'react'
|
|
import { Chat } from '@/features/chat/components/Chat'
|
|
import { Effects } from './effects/Effects'
|
|
import { Admin } from './Admin'
|
|
import { Tools } from './Tools'
|
|
import { Info } from './Info'
|
|
import { HStack } from '@/styled-system/jsx'
|
|
import { useReactionsToolbar } from '@/features/reactions/hooks/useReactionsToolbar'
|
|
import { useRestoreFocus } from '@/hooks/useRestoreFocus'
|
|
|
|
type StyledSidePanelProps = {
|
|
title: string
|
|
ariaLabel: string
|
|
children: ReactNode
|
|
onClose: () => void
|
|
isClosed: boolean
|
|
closeButtonTooltip: string
|
|
isSubmenu: boolean
|
|
onBack: () => void
|
|
backButtonLabel: string
|
|
isReactionToolbarOpen?: boolean
|
|
}
|
|
|
|
const StyledSidePanel = React.forwardRef<HTMLElement, StyledSidePanelProps>(
|
|
(
|
|
{
|
|
title,
|
|
ariaLabel,
|
|
children,
|
|
onClose,
|
|
isClosed,
|
|
isReactionToolbarOpen,
|
|
closeButtonTooltip,
|
|
isSubmenu = false,
|
|
onBack,
|
|
backButtonLabel,
|
|
},
|
|
ref
|
|
) => (
|
|
<aside
|
|
ref={ref}
|
|
tabIndex={-1}
|
|
className={css({
|
|
borderWidth: '1px',
|
|
borderStyle: 'solid',
|
|
borderColor: 'box.border',
|
|
backgroundColor: 'box.bg',
|
|
color: 'box.text',
|
|
borderRadius: 8,
|
|
flex: 1,
|
|
position: 'absolute',
|
|
overflow: 'hidden',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
margin: 'var(--sizes-room-side-panel-margin)',
|
|
marginLeft: 0,
|
|
marginBottom: 0,
|
|
padding: 0,
|
|
gap: 0,
|
|
right: 0,
|
|
top: 0,
|
|
width: 'var(--sizes-room-side-panel)',
|
|
transition: '.5s cubic-bezier(.4,0,.2,1) 5ms',
|
|
'&:focus': {
|
|
outline: 'none',
|
|
},
|
|
})}
|
|
style={{
|
|
transform: isClosed
|
|
? 'translateX(calc(var(--sizes-room-side-panel) + var(--sizes-room-side-panel-margin)))'
|
|
: 'none',
|
|
bottom: isReactionToolbarOpen
|
|
? 'calc( var(--sizes-room-control-bar) + var(--sizes-room-reaction-toolbar-height) + calc(var(--lk-grid-gap) / 2))'
|
|
: 'var(--sizes-room-control-bar)',
|
|
}}
|
|
aria-hidden={isClosed}
|
|
aria-label={ariaLabel}
|
|
>
|
|
<HStack alignItems="center">
|
|
{isSubmenu && (
|
|
<Button
|
|
variant="secondaryText"
|
|
size="sm"
|
|
square
|
|
className={css({ marginRight: '0.5rem', marginLeft: '1rem' })}
|
|
aria-label={backButtonLabel}
|
|
onPress={onBack}
|
|
>
|
|
<RiArrowLeftLine size={20} aria-hidden="true" />
|
|
</Button>
|
|
)}
|
|
<Heading
|
|
slot="title"
|
|
level={1}
|
|
className={text({ variant: 'h2' })}
|
|
style={{
|
|
paddingLeft: isSubmenu ? 0 : '1.5rem',
|
|
paddingTop: '1rem',
|
|
display: isClosed ? 'none' : 'flex',
|
|
justifyContent: 'start',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
{title}
|
|
</Heading>
|
|
</HStack>
|
|
<Div
|
|
position="absolute"
|
|
top="5"
|
|
right="5"
|
|
style={{
|
|
display: isClosed ? 'none' : undefined,
|
|
}}
|
|
>
|
|
<Button
|
|
invisible
|
|
variant="tertiaryText"
|
|
size="xs"
|
|
onPress={onClose}
|
|
aria-label={closeButtonTooltip}
|
|
tooltip={closeButtonTooltip}
|
|
>
|
|
<RiCloseLine />
|
|
</Button>
|
|
</Div>
|
|
{children}
|
|
</aside>
|
|
)
|
|
)
|
|
|
|
StyledSidePanel.displayName = 'StyledSidePanel'
|
|
|
|
type PanelProps = {
|
|
isOpen: boolean
|
|
children: React.ReactNode
|
|
keepAlive?: boolean
|
|
}
|
|
|
|
const Panel = ({ isOpen, keepAlive = false, children }: PanelProps) => (
|
|
<div
|
|
style={{
|
|
display: isOpen ? 'inherit' : 'none',
|
|
flexDirection: 'column',
|
|
overflow: 'hidden',
|
|
flexGrow: 1,
|
|
}}
|
|
>
|
|
{keepAlive || isOpen ? children : null}
|
|
</div>
|
|
)
|
|
export const SidePanel = () => {
|
|
const {
|
|
activePanelId,
|
|
isParticipantsOpen,
|
|
isEffectsOpen,
|
|
isChatOpen,
|
|
isSidePanelOpen,
|
|
isToolsOpen,
|
|
isAdminOpen,
|
|
isInfoOpen,
|
|
isSubPanelOpen,
|
|
activeSubPanelId,
|
|
} = useSidePanel()
|
|
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
|
|
const title = t(`heading.${activeSubPanelId || activePanelId}`)
|
|
|
|
const { isOpen: isReactionToolbarOpen } = useReactionsToolbar()
|
|
|
|
const asideRef = useRef<HTMLElement>(null)
|
|
|
|
const focusAside = useCallback(() => {
|
|
requestAnimationFrame(() => {
|
|
asideRef.current?.focus({ preventScroll: true })
|
|
})
|
|
}, [])
|
|
|
|
const handlePanelOpened = useCallback(() => {
|
|
if (activePanelId === PanelId.CHAT) return
|
|
focusAside()
|
|
}, [activePanelId, focusAside])
|
|
|
|
useRestoreFocus(isSidePanelOpen, {
|
|
onOpened: handlePanelOpened,
|
|
preventScroll: true,
|
|
activeKey: activePanelId,
|
|
})
|
|
|
|
return (
|
|
<StyledSidePanel
|
|
ref={asideRef}
|
|
title={title}
|
|
ariaLabel={t('ariaLabel', { title })}
|
|
onClose={() => {
|
|
layoutStore.activePanelId = null
|
|
layoutStore.activeSubPanelId = null
|
|
}}
|
|
closeButtonTooltip={t('closeButton', {
|
|
content: t(`content.${activeSubPanelId || activePanelId}`),
|
|
})}
|
|
isClosed={!isSidePanelOpen}
|
|
isSubmenu={isSubPanelOpen}
|
|
isReactionToolbarOpen={isReactionToolbarOpen}
|
|
backButtonLabel={t('backToTools')}
|
|
onBack={() => (layoutStore.activeSubPanelId = null)}
|
|
>
|
|
<Panel isOpen={isParticipantsOpen}>
|
|
<ParticipantsList />
|
|
</Panel>
|
|
<Panel isOpen={isEffectsOpen}>
|
|
<Effects />
|
|
</Panel>
|
|
<Panel isOpen={isChatOpen}>
|
|
<Chat />
|
|
</Panel>
|
|
<Panel isOpen={isToolsOpen} keepAlive={true}>
|
|
<Tools />
|
|
</Panel>
|
|
<Panel isOpen={isAdminOpen}>
|
|
<Admin />
|
|
</Panel>
|
|
<Panel isOpen={isInfoOpen}>
|
|
<Info />
|
|
</Panel>
|
|
</StyledSidePanel>
|
|
)
|
|
}
|