🚸(frontend) enhance side panel UX

Implement smooth animations and DOM persistence for sidepanel.
Side panel state should be kept alive, to match initial LiveKit team
intent.

Temporarily, remove chat component. Chat functionality will be absent
until next commits. It will be reintegrated in the side panel.
This commit is contained in:
lebaudantoine
2024-10-13 21:39:07 +02:00
committed by aleb_the_flash
parent 54d4330a97
commit 2a12715673
4 changed files with 79 additions and 38 deletions

View File

@@ -105,7 +105,7 @@ export const MainNotificationToast = () => {
}, [room]) }, [room])
return ( return (
<Div position="absolute" bottom={20} right={5} zIndex={1000}> <Div position="absolute" bottom={0} right={5} zIndex={1000}>
<ToastProvider /> <ToastProvider />
</Div> </Div>
) )

View File

@@ -15,6 +15,7 @@ type StyledSidePanelProps = {
title: string title: string
children: ReactNode children: ReactNode
onClose: () => void onClose: () => void
isClosed: boolean
closeButtonTooltip: string closeButtonTooltip: string
} }
@@ -22,11 +23,11 @@ const StyledSidePanel = ({
title, title,
children, children,
onClose, onClose,
isClosed,
closeButtonTooltip, closeButtonTooltip,
}: StyledSidePanelProps) => ( }: StyledSidePanelProps) => (
<Box <Box
size="sm" size="sm"
minWidth="360px"
className={css({ className={css({
overflow: 'hidden', overflow: 'hidden',
display: 'flex', display: 'flex',
@@ -34,7 +35,16 @@ const StyledSidePanel = ({
margin: '1.5rem 1.5rem 1.5rem 0', margin: '1.5rem 1.5rem 1.5rem 0',
padding: 0, padding: 0,
gap: 0, gap: 0,
right: 0,
top: 0,
bottom: '80px',
width: '360px',
position: 'absolute',
transition: '.5s cubic-bezier(.4,0,.2,1) 5ms',
})} })}
style={{
transform: isClosed ? 'translateX(calc(360px + 1.5rem))' : 'none',
}}
> >
<Heading <Heading
slot="title" slot="title"
@@ -43,11 +53,19 @@ const StyledSidePanel = ({
style={{ style={{
paddingLeft: '1.5rem', paddingLeft: '1.5rem',
paddingTop: '1rem', paddingTop: '1rem',
display: isClosed ? 'none' : undefined,
}} }}
> >
{title} {title}
</Heading> </Heading>
<Div position="absolute" top="5" right="5"> <Div
position="absolute"
top="5"
right="5"
style={{
display: isClosed ? 'none' : undefined,
}}
>
<Button <Button
invisible invisible
size="xs" size="xs"
@@ -62,6 +80,17 @@ const StyledSidePanel = ({
</Box> </Box>
) )
type PanelProps = {
isOpen: boolean;
children: React.ReactNode;
};
const Panel = ({ isOpen, children }: PanelProps) => (
<div style={{ display: isOpen ? 'block' : 'none' }}>
{children}
</div>
)
export const SidePanel = () => { export const SidePanel = () => {
const layoutSnap = useSnapshot(layoutStore) const layoutSnap = useSnapshot(layoutStore)
const sidePanel = layoutSnap.sidePanel const sidePanel = layoutSnap.sidePanel
@@ -69,10 +98,6 @@ export const SidePanel = () => {
const { isParticipantsOpen, isEffectsOpen } = useWidgetInteraction() const { isParticipantsOpen, isEffectsOpen } = useWidgetInteraction()
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' }) const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
if (!sidePanel) {
return
}
return ( return (
<StyledSidePanel <StyledSidePanel
title={t(`heading.${sidePanel}`)} title={t(`heading.${sidePanel}`)}
@@ -80,9 +105,18 @@ export const SidePanel = () => {
closeButtonTooltip={t('closeButton', { closeButtonTooltip={t('closeButton', {
content: t(`content.${sidePanel}`), content: t(`content.${sidePanel}`),
})} })}
isClosed={!sidePanel}
> >
{isParticipantsOpen && <ParticipantsList />} <Panel
{isEffectsOpen && <Effects />} isOpen={isParticipantsOpen}
>
<ParticipantsList />
</Panel>
<Panel
isOpen={isEffectsOpen}
>
<Effects />
</Panel>
</StyledSidePanel> </StyledSidePanel>
) )
} }

View File

@@ -8,7 +8,6 @@ import {
usePersistentUserChoices, usePersistentUserChoices,
} from '@livekit/components-react' } from '@livekit/components-react'
import { mergeProps } from '@/utils/mergeProps.ts'
import { StartMediaButton } from '../components/controls/StartMediaButton' import { StartMediaButton } from '../components/controls/StartMediaButton'
import { useMediaQuery } from '../hooks/useMediaQuery' import { useMediaQuery } from '../hooks/useMediaQuery'
import { OptionsButton } from '../components/controls/Options/OptionsButton' import { OptionsButton } from '../components/controls/Options/OptionsButton'
@@ -18,6 +17,7 @@ import { HandToggle } from '../components/controls/HandToggle'
import { SelectToggleDevice } from '../components/controls/SelectToggleDevice' import { SelectToggleDevice } from '../components/controls/SelectToggleDevice'
import { LeaveButton } from '../components/controls/LeaveButton' import { LeaveButton } from '../components/controls/LeaveButton'
import { ScreenShareToggle } from '../components/controls/ScreenShareToggle' import { ScreenShareToggle } from '../components/controls/ScreenShareToggle'
import { css } from '@/styled-system/css'
/** @public */ /** @public */
export type ControlBarControls = { export type ControlBarControls = {
@@ -63,7 +63,6 @@ export function ControlBar({
variation, variation,
saveUserChoices = true, saveUserChoices = true,
onDeviceError, onDeviceError,
...props
}: ControlBarProps) { }: ControlBarProps) {
const [isChatOpen, setIsChatOpen] = React.useState(false) const [isChatOpen, setIsChatOpen] = React.useState(false)
const layoutContext = useMaybeLayoutContext() const layoutContext = useMaybeLayoutContext()
@@ -82,8 +81,6 @@ export function ControlBar({
const browserSupportsScreenSharing = supportsScreenSharing() const browserSupportsScreenSharing = supportsScreenSharing()
const htmlProps = mergeProps({ className: 'lk-control-bar' }, props)
const { const {
saveAudioInputEnabled, saveAudioInputEnabled,
saveVideoInputEnabled, saveVideoInputEnabled,
@@ -104,7 +101,23 @@ export function ControlBar({
) )
return ( return (
<div {...htmlProps}> <div
className={css({
display: 'flex',
gap: '.5rem',
alignItems: 'center',
justifyContent: 'center',
padding: '.75rem',
borderTop: '1px solid var(--lk-border-color)',
maxHeight: 'var(--lk-control-bar-height)',
height: '80px',
position: 'absolute',
backgroundColor: '#d1d5db',
bottom: 0,
left: 0,
right: 0,
})}
>
<SelectToggleDevice <SelectToggleDevice
source={Track.Source.Microphone} source={Track.Source.Microphone}
onChange={microphoneOnChange} onChange={microphoneOnChange}

View File

@@ -1,6 +1,5 @@
import type { import type {
TrackReferenceOrPlaceholder, TrackReferenceOrPlaceholder,
WidgetState,
} from '@livekit/components-core' } from '@livekit/components-core'
import { import {
isEqualTrackRef, isEqualTrackRef,
@@ -33,7 +32,6 @@ import { FocusLayout } from '../components/FocusLayout'
import { ParticipantTile } from '../components/ParticipantTile' import { ParticipantTile } from '../components/ParticipantTile'
import { SidePanel } from '../components/SidePanel' import { SidePanel } from '../components/SidePanel'
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast' import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
import { Chat } from '@/features/rooms/livekit/prefabs/Chat'
const LayoutWrapper = styled( const LayoutWrapper = styled(
'div', 'div',
@@ -42,7 +40,7 @@ const LayoutWrapper = styled(
position: 'relative', position: 'relative',
display: 'flex', display: 'flex',
width: '100%', width: '100%',
height: 'calc(100% - var(--lk-control-bar-height))', height: '100%',
}, },
}) })
) )
@@ -79,11 +77,6 @@ export function VideoConference({
chatMessageFormatter, chatMessageFormatter,
...props ...props
}: VideoConferenceProps) { }: VideoConferenceProps) {
const [widgetState, setWidgetState] = React.useState<WidgetState>({
showChat: false,
unreadMessages: 0,
showSettings: false,
})
const lastAutoFocusedScreenShareTrack = const lastAutoFocusedScreenShareTrack =
React.useRef<TrackReferenceOrPlaceholder | null>(null) React.useRef<TrackReferenceOrPlaceholder | null>(null)
@@ -95,11 +88,6 @@ export function VideoConference({
{ updateOnlyOn: [RoomEvent.ActiveSpeakersChanged], onlySubscribed: false } { updateOnlyOn: [RoomEvent.ActiveSpeakersChanged], onlySubscribed: false }
) )
const widgetUpdate = (state: WidgetState) => {
log.debug('updating widget state', state)
setWidgetState(state)
}
const layoutContext = useCreateLayoutContext() const layoutContext = useCreateLayoutContext()
const screenShareTracks = tracks const screenShareTracks = tracks
@@ -167,6 +155,8 @@ export function VideoConference({
/* eslint-enable react-hooks/exhaustive-deps */ /* eslint-enable react-hooks/exhaustive-deps */
const layoutSnap = useSnapshot(layoutStore) const layoutSnap = useSnapshot(layoutStore)
// todo - rename this variable
const sidePanel = layoutSnap.sidePanel const sidePanel = layoutSnap.sidePanel
return ( return (
@@ -175,9 +165,17 @@ export function VideoConference({
<LayoutContextProvider <LayoutContextProvider
value={layoutContext} value={layoutContext}
// onPinChange={handleFocusStateChange} // onPinChange={handleFocusStateChange}
onWidgetChange={widgetUpdate}
> >
<div className="lk-video-conference-inner"> <div
// todo - extract these magic values into constant
style={{
position: 'absolute',
inset: sidePanel
? 'var(--lk-grid-gap) calc(358px + 3rem) calc(80px + var(--lk-grid-gap)) 16px'
: 'var(--lk-grid-gap) var(--lk-grid-gap) calc(80px + var(--lk-grid-gap))',
transition: 'inset .5s cubic-bezier(0.4,0,0.2,1) 5ms',
}}
>
<LayoutWrapper> <LayoutWrapper>
<div <div
style={{ display: 'flex', position: 'relative', width: '100%' }} style={{ display: 'flex', position: 'relative', width: '100%' }}
@@ -187,7 +185,7 @@ export function VideoConference({
className="lk-grid-layout-wrapper" className="lk-grid-layout-wrapper"
style={{ height: 'auto' }} style={{ height: 'auto' }}
> >
<GridLayout tracks={tracks}> <GridLayout tracks={tracks} style={{ padding: 0 }}>
<ParticipantTile /> <ParticipantTile />
</GridLayout> </GridLayout>
</div> </div>
@@ -196,7 +194,7 @@ export function VideoConference({
className="lk-focus-layout-wrapper" className="lk-focus-layout-wrapper"
style={{ height: 'auto' }} style={{ height: 'auto' }}
> >
<FocusLayoutContainer> <FocusLayoutContainer style={{ padding: 0 }}>
<CarouselLayout <CarouselLayout
tracks={carouselTracks} tracks={carouselTracks}
style={{ style={{
@@ -209,16 +207,12 @@ export function VideoConference({
</FocusLayoutContainer> </FocusLayoutContainer>
</div> </div>
)} )}
<MainNotificationToast />
</div> </div>
<Chat
style={{ display: widgetState.showChat ? 'grid' : 'none' }}
messageFormatter={chatMessageFormatter}
/>
{sidePanel && <SidePanel />}
</LayoutWrapper> </LayoutWrapper>
<ControlBar /> <MainNotificationToast />
</div> </div>
<ControlBar />
<SidePanel />
</LayoutContextProvider> </LayoutContextProvider>
)} )}
<RoomAudioRenderer /> <RoomAudioRenderer />