🚸(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])
return (
<Div position="absolute" bottom={20} right={5} zIndex={1000}>
<Div position="absolute" bottom={0} right={5} zIndex={1000}>
<ToastProvider />
</Div>
)

View File

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

View File

@@ -8,7 +8,6 @@ import {
usePersistentUserChoices,
} from '@livekit/components-react'
import { mergeProps } from '@/utils/mergeProps.ts'
import { StartMediaButton } from '../components/controls/StartMediaButton'
import { useMediaQuery } from '../hooks/useMediaQuery'
import { OptionsButton } from '../components/controls/Options/OptionsButton'
@@ -18,6 +17,7 @@ import { HandToggle } from '../components/controls/HandToggle'
import { SelectToggleDevice } from '../components/controls/SelectToggleDevice'
import { LeaveButton } from '../components/controls/LeaveButton'
import { ScreenShareToggle } from '../components/controls/ScreenShareToggle'
import { css } from '@/styled-system/css'
/** @public */
export type ControlBarControls = {
@@ -63,7 +63,6 @@ export function ControlBar({
variation,
saveUserChoices = true,
onDeviceError,
...props
}: ControlBarProps) {
const [isChatOpen, setIsChatOpen] = React.useState(false)
const layoutContext = useMaybeLayoutContext()
@@ -82,8 +81,6 @@ export function ControlBar({
const browserSupportsScreenSharing = supportsScreenSharing()
const htmlProps = mergeProps({ className: 'lk-control-bar' }, props)
const {
saveAudioInputEnabled,
saveVideoInputEnabled,
@@ -104,7 +101,23 @@ export function ControlBar({
)
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
source={Track.Source.Microphone}
onChange={microphoneOnChange}

View File

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