♻️(frontend) refactor prejoin layout for better extensibility
Major refactoring of prejoin interface to improve user onboarding and camera/microphone permission handling: * Create extensible hint message system for easier addition of permission-related guidance. * Design flexible layout structure to accommodate future camera/mic options and component selection features. * Establish foundation for comprehensive prejoin redesign In upcoming commits, UX will be enhanced to a smoother user experience when joining calls and requesting media permissions.
This commit is contained in:
committed by
aleb_the_flash
parent
201069aa4c
commit
adb99cc5d9
@@ -8,7 +8,7 @@ import { H } from '@/primitives/H'
|
|||||||
import { SelectToggleDevice } from '../livekit/components/controls/SelectToggleDevice'
|
import { SelectToggleDevice } from '../livekit/components/controls/SelectToggleDevice'
|
||||||
import { Field } from '@/primitives/Field'
|
import { Field } from '@/primitives/Field'
|
||||||
import { Button, Dialog, Text, Form } from '@/primitives'
|
import { Button, Dialog, Text, Form } from '@/primitives'
|
||||||
import { HStack, VStack } from '@/styled-system/jsx'
|
import { VStack } from '@/styled-system/jsx'
|
||||||
import { Heading } from 'react-aria-components'
|
import { Heading } from 'react-aria-components'
|
||||||
import { RiImageCircleAiFill } from '@remixicon/react'
|
import { RiImageCircleAiFill } from '@remixicon/react'
|
||||||
import {
|
import {
|
||||||
@@ -71,36 +71,14 @@ const Effects = ({
|
|||||||
</Text>
|
</Text>
|
||||||
<EffectsConfiguration videoTrack={videoTrack} onSubmit={onSubmit} />
|
<EffectsConfiguration videoTrack={videoTrack} onSubmit={onSubmit} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<div
|
<Button
|
||||||
className={css({
|
variant="whiteCircle"
|
||||||
position: 'absolute',
|
onPress={openDialog}
|
||||||
right: 0,
|
tooltip={t('description')}
|
||||||
bottom: '0',
|
aria-label={t('description')}
|
||||||
padding: '1rem',
|
|
||||||
zIndex: '1',
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<Button
|
<RiImageCircleAiFill size={24} />
|
||||||
variant="whiteCircle"
|
</Button>
|
||||||
onPress={openDialog}
|
|
||||||
tooltip={t('description')}
|
|
||||||
aria-label={t('description')}
|
|
||||||
>
|
|
||||||
<RiImageCircleAiFill size={24} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
height: '20%',
|
|
||||||
backgroundImage:
|
|
||||||
'linear-gradient(0deg, rgba(0,0,0,0.7) 0%, rgba(255,255,255,0) 100%)',
|
|
||||||
borderBottomRadius: '1rem',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -160,12 +138,14 @@ export const Join = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const videoEl = useRef(null)
|
const videoEl = useRef(null)
|
||||||
|
const isVideoInitiated = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const videoElement = videoEl.current as HTMLVideoElement | null
|
const videoElement = videoEl.current as HTMLVideoElement | null
|
||||||
|
|
||||||
const handleVideoLoaded = () => {
|
const handleVideoLoaded = () => {
|
||||||
if (videoElement) {
|
if (videoElement) {
|
||||||
|
isVideoInitiated.current = true
|
||||||
videoElement.style.opacity = '1'
|
videoElement.style.opacity = '1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,6 +220,20 @@ export const Join = ({
|
|||||||
enterRoom()
|
enterRoom()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hintMessage = useMemo(() => {
|
||||||
|
if (!videoEnabled) {
|
||||||
|
return 'cameraDisabled'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isVideoInitiated.current) {
|
||||||
|
return 'cameraStarting'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoTrack && videoEnabled) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}, [videoTrack, videoEnabled])
|
||||||
|
|
||||||
const renderWaitingState = () => {
|
const renderWaitingState = () => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ApiLobbyStatus.TIMEOUT:
|
case ApiLobbyStatus.TIMEOUT:
|
||||||
@@ -322,146 +316,216 @@ export const Join = ({
|
|||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'flex-start',
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%',
|
||||||
|
flexDirection: 'column',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
|
gap: { base: '1rem', sm: '2rem', lg: 0 },
|
||||||
lg: {
|
lg: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
height: 'auto',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: '2rem',
|
|
||||||
padding: '0 2rem',
|
|
||||||
flexDirection: 'column',
|
|
||||||
minWidth: 0,
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
minWidth: 0,
|
||||||
|
maxWidth: '764px',
|
||||||
lg: {
|
lg: {
|
||||||
flexDirection: 'row',
|
height: '540px',
|
||||||
width: 'auto',
|
flexGrow: 1,
|
||||||
height: '570px',
|
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
width: '100%',
|
display: 'inline-flex',
|
||||||
lg: {
|
flexDirection: 'column',
|
||||||
width: '740px',
|
flexGrow: 1,
|
||||||
},
|
minWidth: 0,
|
||||||
|
flexShrink: { base: 0, sm: 1 },
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
borderRadius: '1rem',
|
borderRadius: '1rem',
|
||||||
|
flex: '0 1',
|
||||||
|
minWidth: '320px',
|
||||||
|
margin: {
|
||||||
|
base: '0.5rem',
|
||||||
|
sm: '1rem',
|
||||||
|
lg: '1rem 0.5rem 1rem 1rem',
|
||||||
|
},
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: '100%',
|
display: 'flex',
|
||||||
height: 'auto',
|
flexDirection: 'column',
|
||||||
aspectRatio: 16 / 9,
|
alignItems: 'center',
|
||||||
'--tw-shadow':
|
|
||||||
'0 10px 15px -5px #00000010, 0 4px 5px -6px #00000010',
|
|
||||||
'--tw-shadow-colored':
|
|
||||||
'0 10px 15px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color)',
|
|
||||||
boxShadow:
|
|
||||||
'var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)',
|
|
||||||
backgroundColor: 'black',
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{videoTrack && videoEnabled ? (
|
<div
|
||||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
className={css({
|
||||||
<video
|
position: 'relative',
|
||||||
ref={videoEl}
|
width: '100%',
|
||||||
width="1280"
|
height: 'fit-content',
|
||||||
height="720"
|
aspectRatio: '16 / 9',
|
||||||
className={css({
|
})}
|
||||||
display: 'block',
|
>
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
objectFit: 'cover',
|
|
||||||
transform: 'rotateY(180deg)',
|
|
||||||
opacity: 0,
|
|
||||||
transition: 'opacity 0.3s ease-in-out',
|
|
||||||
borderRadius: '1rem',
|
|
||||||
})}
|
|
||||||
disablePictureInPicture
|
|
||||||
disableRemotePlayback
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
|
backgroundColor: 'black',
|
||||||
|
position: 'absolute',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
top: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
color: 'white',
|
overflow: 'hidden',
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<p
|
<div
|
||||||
|
aria-label={t(
|
||||||
|
`videoPreview.${videoEnabled ? 'enabled' : 'disabled'}`
|
||||||
|
)}
|
||||||
|
role="status"
|
||||||
className={css({
|
className={css({
|
||||||
fontSize: '24px',
|
position: 'absolute',
|
||||||
fontWeight: '300',
|
top: 0,
|
||||||
|
width: '100%',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!videoEnabled && t('cameraDisabled')}
|
<div
|
||||||
{videoEnabled && !videoTrack && t('cameraStarting')}
|
className={css({
|
||||||
</p>
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
aspectRatio: '16 / 9',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-2px',
|
||||||
|
left: '-2px',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
transform: 'scale(1.02)',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||||
|
<video
|
||||||
|
ref={videoEl}
|
||||||
|
width="1280"
|
||||||
|
height="720"
|
||||||
|
style={{
|
||||||
|
display: !videoEnabled ? 'none' : undefined,
|
||||||
|
}}
|
||||||
|
className={css({
|
||||||
|
position: 'absolute',
|
||||||
|
transform: 'rotateY(180deg)',
|
||||||
|
opacity: 0,
|
||||||
|
height: '100%',
|
||||||
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
|
objectFit: 'cover',
|
||||||
|
})}
|
||||||
|
disablePictureInPicture
|
||||||
|
disableRemotePlayback
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textAlign: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '0.24rem',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className={css({
|
||||||
|
fontWeight: '400',
|
||||||
|
fontSize: { base: '1rem', sm: '1.25rem', lg: '1.5rem' },
|
||||||
|
textWrap: 'balance',
|
||||||
|
color: 'white',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{hintMessage && t(hintMessage)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div
|
||||||
<Effects
|
className={css({
|
||||||
videoTrack={videoTrack}
|
position: 'absolute',
|
||||||
onSubmit={(processor) =>
|
right: '1rem',
|
||||||
saveProcessorSerialized(processor?.serialize())
|
bottom: '1rem',
|
||||||
}
|
zIndex: '1',
|
||||||
/>
|
})}
|
||||||
|
>
|
||||||
|
<Effects
|
||||||
|
videoTrack={videoTrack}
|
||||||
|
onSubmit={(processor) =>
|
||||||
|
saveProcessorSerialized(processor?.serialize())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '2%',
|
||||||
|
width: '80%',
|
||||||
|
marginX: 'auto',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<SelectToggleDevice
|
||||||
|
source={Track.Source.Microphone}
|
||||||
|
initialState={audioEnabled}
|
||||||
|
track={audioTrack}
|
||||||
|
initialDeviceId={audioDeviceId}
|
||||||
|
onChange={(enabled) => saveAudioInputEnabled(enabled)}
|
||||||
|
onDeviceError={(error) => console.error(error)}
|
||||||
|
onActiveDeviceChange={(deviceId) =>
|
||||||
|
saveAudioInputDeviceId(deviceId ?? '')
|
||||||
|
}
|
||||||
|
variant="tertiary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<SelectToggleDevice
|
||||||
|
source={Track.Source.Camera}
|
||||||
|
initialState={videoEnabled}
|
||||||
|
track={videoTrack}
|
||||||
|
initialDeviceId={videoDeviceId}
|
||||||
|
onChange={(enabled) => saveVideoInputEnabled(enabled)}
|
||||||
|
onDeviceError={(error) => console.error(error)}
|
||||||
|
onActiveDeviceChange={(deviceId) =>
|
||||||
|
saveVideoInputDeviceId(deviceId ?? '')
|
||||||
|
}
|
||||||
|
variant="tertiary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HStack justify="center" padding={1.5}>
|
|
||||||
<SelectToggleDevice
|
|
||||||
source={Track.Source.Microphone}
|
|
||||||
initialState={audioEnabled}
|
|
||||||
track={audioTrack}
|
|
||||||
initialDeviceId={audioDeviceId}
|
|
||||||
onChange={(enabled) => saveAudioInputEnabled(enabled)}
|
|
||||||
onDeviceError={(error) => console.error(error)}
|
|
||||||
onActiveDeviceChange={(deviceId) =>
|
|
||||||
saveAudioInputDeviceId(deviceId ?? '')
|
|
||||||
}
|
|
||||||
variant="tertiary"
|
|
||||||
/>
|
|
||||||
<SelectToggleDevice
|
|
||||||
source={Track.Source.Camera}
|
|
||||||
initialState={videoEnabled}
|
|
||||||
track={videoTrack}
|
|
||||||
initialDeviceId={videoDeviceId}
|
|
||||||
onChange={(enabled) => saveVideoInputEnabled(enabled)}
|
|
||||||
onDeviceError={(error) => console.error(error)}
|
|
||||||
onActiveDeviceChange={(deviceId) =>
|
|
||||||
saveVideoInputDeviceId(deviceId ?? '')
|
|
||||||
}
|
|
||||||
variant="tertiary"
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
width: '100%',
|
|
||||||
flexShrink: 0,
|
|
||||||
padding: '0',
|
|
||||||
sm: {
|
|
||||||
width: '448px',
|
|
||||||
padding: '0 3rem 9rem 3rem',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{renderWaitingState()}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
flex: '0 0 448px',
|
||||||
|
position: 'relative',
|
||||||
|
margin: '1rem 1rem 1rem 0.5rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{renderWaitingState()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Screen>
|
</Screen>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user