🚸(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:
committed by
aleb_the_flash
parent
54d4330a97
commit
2a12715673
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user