Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 076d84a5bc |
@@ -32,10 +32,10 @@ const Columns = ({ children }: { children?: ReactNode }) => {
|
||||
justifyContent: 'normal',
|
||||
padding: '0 1rem',
|
||||
width: 'calc(100% - 2rem)',
|
||||
_motionReduce: {
|
||||
'@media(prefers-reduced-motion: reduce)': {
|
||||
opacity: 1,
|
||||
},
|
||||
_motionSafe: {
|
||||
'@media(prefers-reduced-motion: no-preference)': {
|
||||
opacity: 0,
|
||||
animation: '.5s ease-in fade 0s forwards',
|
||||
},
|
||||
|
||||
@@ -1,27 +1,8 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
ParticipantPlaceholder,
|
||||
usePersistentUserChoices,
|
||||
usePreviewTracks,
|
||||
type LocalUserChoices,
|
||||
} from '@livekit/components-react'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { log } from '@livekit/components-core'
|
||||
import { defaultUserChoices } from '@livekit/components-core'
|
||||
import { PreJoin, type LocalUserChoices } from '@livekit/components-react'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import { CenteredContent } from '@/layout/CenteredContent'
|
||||
import { useUser } from '@/features/auth'
|
||||
import React from 'react'
|
||||
import {
|
||||
facingModeFromLocalTrack,
|
||||
LocalVideoTrack,
|
||||
Track,
|
||||
} from 'livekit-client'
|
||||
import { H } from '@/primitives/H'
|
||||
import { SelectToggleDevice } from '../livekit/components/controls/SelectToggleDevice'
|
||||
import { Field } from '@/primitives/Field'
|
||||
import { Button } from '@/primitives'
|
||||
|
||||
const onError = (e: Error) => console.error('ERROR', e)
|
||||
|
||||
export const Join = ({
|
||||
onSubmit,
|
||||
@@ -30,349 +11,20 @@ export const Join = ({
|
||||
}) => {
|
||||
const { t } = useTranslation('rooms')
|
||||
const { user } = useUser()
|
||||
const defaults: Partial<LocalUserChoices> = { username: user?.full_name }
|
||||
const persistUserChoices = true
|
||||
const joinLabel = t('join.joinLabel')
|
||||
const userLabel = t('join.usernameLabel')
|
||||
|
||||
const [userChoices, setUserChoices] = React.useState(defaultUserChoices)
|
||||
|
||||
// TODO: Remove and pipe `defaults` object directly into `usePersistentUserChoices` once we fully switch from type `LocalUserChoices` to `UserChoices`.
|
||||
const partialDefaults: Partial<LocalUserChoices> = {
|
||||
...(defaults.audioDeviceId !== undefined && {
|
||||
audioDeviceId: defaults.audioDeviceId,
|
||||
}),
|
||||
...(defaults.videoDeviceId !== undefined && {
|
||||
videoDeviceId: defaults.videoDeviceId,
|
||||
}),
|
||||
...(defaults.audioEnabled !== undefined && {
|
||||
audioEnabled: defaults.audioEnabled,
|
||||
}),
|
||||
...(defaults.videoEnabled !== undefined && {
|
||||
videoEnabled: defaults.videoEnabled,
|
||||
}),
|
||||
...(defaults.username !== undefined && { username: defaults.username }),
|
||||
}
|
||||
|
||||
const {
|
||||
userChoices: initialUserChoices,
|
||||
saveAudioInputDeviceId,
|
||||
saveAudioInputEnabled,
|
||||
saveVideoInputDeviceId,
|
||||
saveVideoInputEnabled,
|
||||
saveUsername,
|
||||
} = usePersistentUserChoices({
|
||||
defaults: partialDefaults,
|
||||
preventSave: !persistUserChoices,
|
||||
preventLoad: !persistUserChoices,
|
||||
})
|
||||
|
||||
// Initialize device settings
|
||||
const [audioEnabled, setAudioEnabled] = React.useState<boolean>(
|
||||
initialUserChoices.audioEnabled
|
||||
)
|
||||
const [videoEnabled, setVideoEnabled] = React.useState<boolean>(
|
||||
initialUserChoices.videoEnabled
|
||||
)
|
||||
const [audioDeviceId, setAudioDeviceId] = React.useState<string>(
|
||||
initialUserChoices.audioDeviceId
|
||||
)
|
||||
const [videoDeviceId, setVideoDeviceId] = React.useState<string>(
|
||||
initialUserChoices.videoDeviceId
|
||||
)
|
||||
const [username, setUsername] = React.useState(initialUserChoices.username)
|
||||
|
||||
// Save user choices to persistent storage.
|
||||
React.useEffect(() => {
|
||||
saveAudioInputEnabled(audioEnabled)
|
||||
}, [audioEnabled, saveAudioInputEnabled])
|
||||
React.useEffect(() => {
|
||||
saveVideoInputEnabled(videoEnabled)
|
||||
}, [videoEnabled, saveVideoInputEnabled])
|
||||
React.useEffect(() => {
|
||||
saveAudioInputDeviceId(audioDeviceId)
|
||||
}, [audioDeviceId, saveAudioInputDeviceId])
|
||||
React.useEffect(() => {
|
||||
saveVideoInputDeviceId(videoDeviceId)
|
||||
}, [videoDeviceId, saveVideoInputDeviceId])
|
||||
React.useEffect(() => {
|
||||
saveUsername(username)
|
||||
}, [username, saveUsername])
|
||||
|
||||
const tracks = usePreviewTracks(
|
||||
{
|
||||
audio: audioEnabled
|
||||
? { deviceId: initialUserChoices.audioDeviceId }
|
||||
: false,
|
||||
video: videoEnabled
|
||||
? { deviceId: initialUserChoices.videoDeviceId }
|
||||
: false,
|
||||
},
|
||||
onError
|
||||
)
|
||||
|
||||
const videoEl = React.useRef(null)
|
||||
|
||||
const videoTrack = React.useMemo(
|
||||
() =>
|
||||
tracks?.filter(
|
||||
(track) => track.kind === Track.Kind.Video
|
||||
)[0] as LocalVideoTrack,
|
||||
[tracks]
|
||||
)
|
||||
|
||||
const audioTrack = React.useMemo(
|
||||
() =>
|
||||
tracks?.filter(
|
||||
(track) => track.kind === Track.Kind.Audio
|
||||
)[0] as LocalVideoTrack,
|
||||
[tracks]
|
||||
)
|
||||
|
||||
const facingMode = React.useMemo(() => {
|
||||
if (videoTrack) {
|
||||
const { facingMode } = facingModeFromLocalTrack(videoTrack)
|
||||
return facingMode
|
||||
} else {
|
||||
return 'undefined'
|
||||
}
|
||||
}, [videoTrack])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (videoEl.current && videoTrack) {
|
||||
videoTrack.unmute()
|
||||
videoTrack.attach(videoEl.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
videoTrack?.detach()
|
||||
}
|
||||
}, [videoTrack])
|
||||
|
||||
const [isValid, setIsValid] = React.useState<boolean>()
|
||||
|
||||
const handleValidation = React.useCallback((values: LocalUserChoices) => {
|
||||
return values.username !== ''
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
const newUserChoices = {
|
||||
username,
|
||||
videoEnabled,
|
||||
videoDeviceId,
|
||||
audioEnabled,
|
||||
audioDeviceId,
|
||||
}
|
||||
setUserChoices(newUserChoices)
|
||||
setIsValid(handleValidation(newUserChoices))
|
||||
}, [
|
||||
username,
|
||||
videoEnabled,
|
||||
handleValidation,
|
||||
audioEnabled,
|
||||
audioDeviceId,
|
||||
videoDeviceId,
|
||||
])
|
||||
|
||||
function handleSubmit() {
|
||||
if (handleValidation(userChoices)) {
|
||||
if (typeof onSubmit === 'function') {
|
||||
onSubmit(userChoices)
|
||||
}
|
||||
} else {
|
||||
log.warn('Validation failed with: ', userChoices)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen footer={false}>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
flexGrow: 1,
|
||||
lg: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
height: 'auto',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '2rem',
|
||||
padding: '0 2rem',
|
||||
flexDirection: 'column',
|
||||
minWidth: 0,
|
||||
width: '100%',
|
||||
lg: {
|
||||
flexDirection: 'row',
|
||||
width: 'auto',
|
||||
height: '570px',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
lg: {
|
||||
width: '740px',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
borderRadius: '1rem',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
aspectRatio: 16 / 9,
|
||||
'--tw-shadow':
|
||||
'0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a',
|
||||
'--tw-shadow-colored':
|
||||
'0 20px 25px -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)',
|
||||
})}
|
||||
>
|
||||
{videoTrack && (
|
||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
||||
<video
|
||||
ref={videoEl}
|
||||
width="1280"
|
||||
height="720"
|
||||
data-lk-facing-mode={facingMode}
|
||||
className={css({
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
transform: 'rotateY(180deg)',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{(!videoTrack || !videoEnabled) && (
|
||||
<div
|
||||
id="container"
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: '#000',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
})}
|
||||
>
|
||||
<ParticipantPlaceholder
|
||||
className={css({
|
||||
maxWidth: '100%',
|
||||
height: '70%',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="lk-button-group-container"></div>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '1.5rem',
|
||||
gap: '1rem',
|
||||
})}
|
||||
>
|
||||
<SelectToggleDevice
|
||||
source={Track.Source.Microphone}
|
||||
initialState={audioEnabled}
|
||||
track={audioTrack}
|
||||
initialDeviceId={audioDeviceId}
|
||||
onChange={(enabled) => setAudioEnabled(enabled)}
|
||||
onDeviceError={(error) => console.error(error)}
|
||||
onActiveDeviceChange={(deviceId) =>
|
||||
setAudioDeviceId(deviceId ?? '')
|
||||
}
|
||||
variant="tertiary"
|
||||
/>
|
||||
<SelectToggleDevice
|
||||
source={Track.Source.Camera}
|
||||
initialState={videoEnabled}
|
||||
track={videoTrack}
|
||||
initialDeviceId={videoDeviceId}
|
||||
onChange={(enabled) => {
|
||||
setVideoEnabled(enabled)
|
||||
}}
|
||||
onDeviceError={(error) => console.error(error)}
|
||||
onActiveDeviceChange={(deviceId) =>
|
||||
setVideoDeviceId(deviceId ?? '')
|
||||
}
|
||||
variant="tertiary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
flexShrink: 0,
|
||||
padding: '0',
|
||||
sm: {
|
||||
width: '448px',
|
||||
padding: '0 3rem 9rem 3rem',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<form
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
})}
|
||||
>
|
||||
<H lvl={1} className={css({ marginBottom: 0 })}>
|
||||
{t('join.heading')}
|
||||
</H>
|
||||
<Field
|
||||
type="text"
|
||||
label={userLabel}
|
||||
defaultValue={username}
|
||||
onChange={(value) => setUsername(value)}
|
||||
validate={(value) => {
|
||||
return !value ? <p>{t('join.errors.usernameEmpty')}</p> : null
|
||||
}}
|
||||
className={css({
|
||||
width: '100%',
|
||||
})}
|
||||
wrapperProps={{
|
||||
noMargin: true,
|
||||
fullWidth: true,
|
||||
}}
|
||||
labelProps={{
|
||||
center: true,
|
||||
}}
|
||||
maxLength={50}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant={'primary'}
|
||||
onPress={handleSubmit}
|
||||
isDisabled={!isValid}
|
||||
fullWidth
|
||||
>
|
||||
{joinLabel}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Screen layout="centered" footer={false}>
|
||||
<CenteredContent title={t('join.heading')}>
|
||||
<PreJoin
|
||||
persistUserChoices
|
||||
onSubmit={onSubmit}
|
||||
micLabel={t('join.audioinput.label')}
|
||||
camLabel={t('join.videoinput.label')}
|
||||
joinLabel={t('join.joinLabel')}
|
||||
userLabel={t('join.usernameLabel')}
|
||||
defaults={{ username: user?.full_name }}
|
||||
/>
|
||||
</CenteredContent>
|
||||
</Screen>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,14 +13,12 @@ import {
|
||||
RiVideoOffLine,
|
||||
RiVideoOnLine,
|
||||
} from '@remixicon/react'
|
||||
import { LocalAudioTrack, LocalVideoTrack, Track } from 'livekit-client'
|
||||
import { Track } from 'livekit-client'
|
||||
|
||||
import { Shortcut } from '@/features/shortcuts/types'
|
||||
|
||||
import { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice.tsx'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export type ToggleSource = Exclude<
|
||||
Track.Source,
|
||||
@@ -67,22 +65,16 @@ const selectToggleDeviceConfig: SelectToggleDeviceConfigMap = {
|
||||
|
||||
type SelectToggleDeviceProps<T extends ToggleSource> =
|
||||
UseTrackToggleProps<T> & {
|
||||
track?: LocalAudioTrack | LocalVideoTrack | undefined
|
||||
initialDeviceId?: string
|
||||
onActiveDeviceChange: (deviceId: string) => void
|
||||
source: SelectToggleSource
|
||||
variant?: NonNullable<ButtonRecipeProps>['variant']
|
||||
menuVariant?: 'dark' | 'light'
|
||||
hideMenu?: boolean
|
||||
variant?: 'dark' | 'light'
|
||||
}
|
||||
|
||||
export const SelectToggleDevice = <T extends ToggleSource>({
|
||||
track,
|
||||
initialDeviceId,
|
||||
onActiveDeviceChange,
|
||||
hideMenu,
|
||||
variant = 'primaryDark',
|
||||
menuVariant = 'light',
|
||||
variant = 'light',
|
||||
...props
|
||||
}: SelectToggleDeviceProps<T>) => {
|
||||
const config = selectToggleDeviceConfig[props.source]
|
||||
@@ -93,19 +85,7 @@ export const SelectToggleDevice = <T extends ToggleSource>({
|
||||
const trackProps = useTrackToggle(props)
|
||||
|
||||
const { devices, activeDeviceId, setActiveMediaDevice } =
|
||||
useMediaDeviceSelect({ kind: config.kind, track })
|
||||
|
||||
/**
|
||||
* When providing only track outside of a room context, activeDeviceId is undefined.
|
||||
* So we need to initialize it with the initialDeviceId.
|
||||
* nb: I don't understand why useMediaDeviceSelect cannot infer it from track device id.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (initialDeviceId !== undefined) {
|
||||
setActiveMediaDevice(initialDeviceId)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [setActiveMediaDevice])
|
||||
useMediaDeviceSelect({ kind: config.kind })
|
||||
|
||||
const selectLabel = t('choose', { keyPrefix: `join.${config.kind}` })
|
||||
|
||||
@@ -119,7 +99,6 @@ export const SelectToggleDevice = <T extends ToggleSource>({
|
||||
<ToggleDevice
|
||||
{...trackProps}
|
||||
config={config}
|
||||
variant={variant}
|
||||
toggleButtonProps={{
|
||||
...(hideMenu
|
||||
? {
|
||||
@@ -129,13 +108,13 @@ export const SelectToggleDevice = <T extends ToggleSource>({
|
||||
}}
|
||||
/>
|
||||
{!hideMenu && (
|
||||
<Menu variant={menuVariant}>
|
||||
<Menu variant={variant}>
|
||||
<Button
|
||||
tooltip={selectLabel}
|
||||
aria-label={selectLabel}
|
||||
groupPosition="right"
|
||||
square
|
||||
variant={trackProps.enabled ? variant : 'error2'}
|
||||
variant={trackProps.enabled ? 'primaryDark' : 'error2'}
|
||||
>
|
||||
<RiArrowDownSLine />
|
||||
</Button>
|
||||
@@ -149,7 +128,7 @@ export const SelectToggleDevice = <T extends ToggleSource>({
|
||||
setActiveMediaDevice(value as string)
|
||||
onActiveDeviceChange(value as string)
|
||||
}}
|
||||
variant={menuVariant}
|
||||
variant={variant}
|
||||
/>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
@@ -6,19 +6,13 @@ import { useTranslation } from 'react-i18next'
|
||||
import { SelectToggleDeviceConfig } from './SelectToggleDevice'
|
||||
import useLongPress from '@/features/shortcuts/useLongPress'
|
||||
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
|
||||
import {
|
||||
useIsSpeaking,
|
||||
useLocalParticipant,
|
||||
useMaybeRoomContext,
|
||||
} from '@livekit/components-react'
|
||||
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
||||
import { useIsSpeaking, useLocalParticipant } from '@livekit/components-react'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
|
||||
export type ToggleDeviceProps = {
|
||||
enabled: boolean
|
||||
toggle: () => void
|
||||
config: SelectToggleDeviceConfig
|
||||
variant?: NonNullable<ButtonRecipeProps>['variant']
|
||||
toggleButtonProps?: Partial<ToggleButtonProps>
|
||||
}
|
||||
|
||||
@@ -26,7 +20,6 @@ export const ToggleDevice = ({
|
||||
config,
|
||||
enabled,
|
||||
toggle,
|
||||
variant = 'primaryDark',
|
||||
toggleButtonProps,
|
||||
}: ToggleDeviceProps) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||
@@ -58,15 +51,17 @@ export const ToggleDevice = ({
|
||||
|
||||
const Icon = enabled ? iconOn : iconOff
|
||||
|
||||
const context = useMaybeRoomContext()
|
||||
if (kind === 'audioinput' && pushToTalk && context) {
|
||||
return <ActiveSpeakerWrapper />
|
||||
const { localParticipant } = useLocalParticipant()
|
||||
const isSpeaking = useIsSpeaking(localParticipant)
|
||||
|
||||
if (kind === 'audioinput' && pushToTalk) {
|
||||
return <ActiveSpeaker isSpeaking={isSpeaking} pushToTalk />
|
||||
}
|
||||
|
||||
return (
|
||||
<ToggleButton
|
||||
isSelected={!enabled}
|
||||
variant={enabled ? variant : 'error2'}
|
||||
variant={enabled ? 'primaryDark' : 'error2'}
|
||||
shySelected
|
||||
onPress={() => toggle()}
|
||||
aria-label={toggleLabel}
|
||||
@@ -78,9 +73,3 @@ export const ToggleDevice = ({
|
||||
</ToggleButton>
|
||||
)
|
||||
}
|
||||
|
||||
const ActiveSpeakerWrapper = () => {
|
||||
const { localParticipant } = useLocalParticipant()
|
||||
const isSpeaking = useIsSpeaking(localParticipant)
|
||||
return <ActiveSpeaker isSpeaking={isSpeaking} pushToTalk />
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { css } from '@/styled-system/css'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
|
||||
export const TranscriptToggle = ({
|
||||
variant = 'primaryDark',
|
||||
variant = 'primaryTextDark',
|
||||
onPress,
|
||||
...props
|
||||
}: ToggleButtonProps) => {
|
||||
|
||||
@@ -8,10 +8,8 @@ import { HandToggle } from '../../components/controls/HandToggle'
|
||||
import { ScreenShareToggle } from '../../components/controls/ScreenShareToggle'
|
||||
import { OptionsButton } from '../../components/controls/Options/OptionsButton'
|
||||
import { StartMediaButton } from '../../components/controls/StartMediaButton'
|
||||
import { ChatToggle } from '../../components/controls/ChatToggle'
|
||||
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
|
||||
import { SupportToggle } from '../../components/controls/SupportToggle'
|
||||
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
|
||||
import { MoreOptions } from './MoreOptions'
|
||||
import { useRef } from 'react'
|
||||
|
||||
export function DesktopControlBar({
|
||||
onDeviceError,
|
||||
@@ -21,9 +19,11 @@ export function DesktopControlBar({
|
||||
saveVideoInputDeviceId,
|
||||
}: ControlBarAuxProps) {
|
||||
const browserSupportsScreenSharing = supportsScreenSharing()
|
||||
const desktopControlBarEl = useRef<HTMLDivElement>(null)
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={desktopControlBarEl}
|
||||
className={css({
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
@@ -62,7 +62,7 @@ export function DesktopControlBar({
|
||||
onActiveDeviceChange={(deviceId) =>
|
||||
saveAudioInputDeviceId(deviceId ?? '')
|
||||
}
|
||||
menuVariant="dark"
|
||||
variant="dark"
|
||||
/>
|
||||
<SelectToggleDevice
|
||||
source={Track.Source.Camera}
|
||||
@@ -73,7 +73,7 @@ export function DesktopControlBar({
|
||||
onActiveDeviceChange={(deviceId) =>
|
||||
saveVideoInputDeviceId(deviceId ?? '')
|
||||
}
|
||||
menuVariant="dark"
|
||||
variant="dark"
|
||||
/>
|
||||
{browserSupportsScreenSharing && (
|
||||
<ScreenShareToggle
|
||||
@@ -87,21 +87,7 @@ export function DesktopControlBar({
|
||||
<LeaveButton />
|
||||
<StartMediaButton />
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
flex: '1 1 33%',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
paddingRight: '0.25rem',
|
||||
})}
|
||||
>
|
||||
<ChatToggle />
|
||||
<ParticipantsToggle />
|
||||
<TranscriptToggle />
|
||||
<SupportToggle />
|
||||
</div>
|
||||
<MoreOptions parentElement={desktopControlBarEl} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { css } from '@/styled-system/css'
|
||||
import { ChatToggle } from '../../components/controls/ChatToggle'
|
||||
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
|
||||
import { SupportToggle } from '../../components/controls/SupportToggle'
|
||||
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
|
||||
import { useSize } from '../../hooks/useResizeObserver'
|
||||
import { useState, RefObject } from 'react'
|
||||
import { Dialog, DialogTrigger, Popover } from 'react-aria-components'
|
||||
import { Button } from '@/primitives'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const CONTROL_BAR_BREAKPOINT = 750
|
||||
|
||||
const NavigationControls = ({ onPress }: Partial<ToggleButtonProps>) => (
|
||||
<>
|
||||
<ChatToggle onPress={onPress} />
|
||||
<ParticipantsToggle onPress={onPress} />
|
||||
<TranscriptToggle onPress={onPress} />
|
||||
<SupportToggle onPress={onPress} />
|
||||
</>
|
||||
)
|
||||
|
||||
export const LateralMenu = () => {
|
||||
const { t } = useTranslation('rooms')
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const handlePress = () => setIsOpen(!isOpen)
|
||||
const handleClose = () => setIsOpen(false)
|
||||
|
||||
return (
|
||||
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||
<Button
|
||||
square
|
||||
variant="secondaryDark"
|
||||
aria-label={t('controls.moreOptions')}
|
||||
tooltip={t('controls.moreOptions')}
|
||||
onPress={handlePress}
|
||||
>
|
||||
{isOpen ? <RiArrowDownSLine /> : <RiArrowUpSLine />}
|
||||
</Button>
|
||||
<Popover>
|
||||
<Dialog
|
||||
className={css({
|
||||
width: '65px',
|
||||
backgroundColor: 'primaryDark.50',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
borderRadius: '4px',
|
||||
paddingTop: '10px',
|
||||
gap: '0.5rem',
|
||||
})}
|
||||
>
|
||||
<NavigationControls onPress={handleClose} />
|
||||
</Dialog>
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
export const MoreOptions = ({
|
||||
parentElement,
|
||||
}: {
|
||||
parentElement: RefObject<HTMLDivElement>
|
||||
}) => {
|
||||
const { width: parentWidth } = useSize(parentElement)
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
flex: '1 1 33%',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
paddingRight: '0.25rem',
|
||||
})}
|
||||
>
|
||||
{parentWidth > CONTROL_BAR_BREAKPOINT ? (
|
||||
<NavigationControls />
|
||||
) : (
|
||||
<LateralMenu />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -24,10 +24,7 @@
|
||||
"toggleOff": "",
|
||||
"toggleOn": "",
|
||||
"usernameHint": "",
|
||||
"usernameLabel": "",
|
||||
"errors": {
|
||||
"usernameEmpty": ""
|
||||
}
|
||||
"usernameLabel": ""
|
||||
},
|
||||
"leaveRoomPrompt": "",
|
||||
"shareDialog": {
|
||||
@@ -78,7 +75,8 @@
|
||||
"open": "",
|
||||
"closed": ""
|
||||
},
|
||||
"support": ""
|
||||
"support": "",
|
||||
"moreOptions": ""
|
||||
},
|
||||
"options": {
|
||||
"buttonLabel": "",
|
||||
|
||||
@@ -18,16 +18,13 @@
|
||||
"enable": "Enable microphone",
|
||||
"label": "Microphone"
|
||||
},
|
||||
"heading": "Join the meeting",
|
||||
"heading": "Verify your settings",
|
||||
"joinLabel": "Join",
|
||||
"joinMeeting": "Join meeting",
|
||||
"toggleOff": "Click to turn off",
|
||||
"toggleOn": "Click to turn on",
|
||||
"usernameHint": "Shown to other participants",
|
||||
"usernameLabel": "Your name",
|
||||
"errors": {
|
||||
"usernameEmpty": "Your name cannot be empty"
|
||||
}
|
||||
"usernameLabel": "Your name"
|
||||
},
|
||||
"leaveRoomPrompt": "This will make you leave the meeting.",
|
||||
"shareDialog": {
|
||||
@@ -77,7 +74,8 @@
|
||||
"open": "Hide AI assistant",
|
||||
"closed": "Show AI assistant"
|
||||
},
|
||||
"support": "Support"
|
||||
"support": "Support",
|
||||
"moreOptions": "More options"
|
||||
},
|
||||
"options": {
|
||||
"buttonLabel": "More Options",
|
||||
|
||||
@@ -18,16 +18,13 @@
|
||||
"enable": "Activer le micro",
|
||||
"label": "Microphone"
|
||||
},
|
||||
"heading": "Rejoindre la réunion",
|
||||
"heading": "Vérifiez vos paramètres",
|
||||
"joinLabel": "Rejoindre",
|
||||
"joinMeeting": "Rejoindre la réjoindre",
|
||||
"toggleOff": "Cliquez pour désactiver",
|
||||
"toggleOn": "Cliquez pour activer",
|
||||
"usernameHint": "Affiché aux autres participants",
|
||||
"usernameLabel": "Votre nom",
|
||||
"errors": {
|
||||
"usernameEmpty": "Votre nom ne peut pas être vide"
|
||||
}
|
||||
"usernameLabel": "Votre nom"
|
||||
},
|
||||
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
|
||||
"shareDialog": {
|
||||
@@ -77,7 +74,8 @@
|
||||
"open": "Masquer l'assistant IA",
|
||||
"closed": "Afficher l'assistant IA"
|
||||
},
|
||||
"support": "Support"
|
||||
"support": "Support",
|
||||
"moreOptions": "Plus d'options"
|
||||
},
|
||||
"options": {
|
||||
"buttonLabel": "Plus d'options",
|
||||
|
||||
@@ -25,31 +25,11 @@ const FieldWrapper = styled('div', {
|
||||
marginBottom: 'textfield',
|
||||
minWidth: 0,
|
||||
},
|
||||
variants: {
|
||||
noMargin: {
|
||||
true: {
|
||||
marginBottom: 0,
|
||||
},
|
||||
},
|
||||
fullWidth: {
|
||||
true: {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const StyledLabel = styled(Label, {
|
||||
base: {
|
||||
display: 'block',
|
||||
fontSize: '0.75rem',
|
||||
},
|
||||
variants: {
|
||||
center: {
|
||||
true: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -100,8 +80,6 @@ type FieldProps<T extends object> = (
|
||||
) & {
|
||||
label: string
|
||||
description?: string
|
||||
wrapperProps?: React.ComponentProps<typeof FieldWrapper>
|
||||
labelProps?: React.ComponentProps<typeof StyledLabel>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,7 +104,7 @@ export const Field = <T extends object>({
|
||||
}: FieldProps<T>) => {
|
||||
const LabelAndDescription = (
|
||||
<>
|
||||
<StyledLabel {...props.labelProps}>{label}</StyledLabel>
|
||||
<StyledLabel>{label}</StyledLabel>
|
||||
<FieldDescription slot="description">{description}</FieldDescription>
|
||||
</>
|
||||
)
|
||||
@@ -140,7 +118,7 @@ export const Field = <T extends object>({
|
||||
|
||||
if (type === 'text') {
|
||||
return (
|
||||
<FieldWrapper {...props.wrapperProps}>
|
||||
<FieldWrapper>
|
||||
<RACTextField
|
||||
validate={validate as unknown as TextFieldProps['validate']}
|
||||
{...(props as PartialTextFieldProps)}
|
||||
@@ -155,7 +133,7 @@ export const Field = <T extends object>({
|
||||
|
||||
if (type === 'checkbox') {
|
||||
return (
|
||||
<FieldWrapper {...props.wrapperProps}>
|
||||
<FieldWrapper>
|
||||
<Checkbox
|
||||
validate={validate as unknown as CheckboxProps['validate']}
|
||||
description={description}
|
||||
@@ -169,7 +147,7 @@ export const Field = <T extends object>({
|
||||
|
||||
if (type === 'checkboxGroup') {
|
||||
return (
|
||||
<FieldWrapper {...props.wrapperProps}>
|
||||
<FieldWrapper>
|
||||
<CheckboxGroup
|
||||
validate={validate as unknown as CheckboxGroupProps['validate']}
|
||||
{...(props as PartialCheckboxGroupProps)}
|
||||
@@ -192,7 +170,7 @@ export const Field = <T extends object>({
|
||||
|
||||
if (type === 'radioGroup') {
|
||||
return (
|
||||
<FieldWrapper {...props.wrapperProps}>
|
||||
<FieldWrapper>
|
||||
<RadioGroup
|
||||
validate={validate as unknown as RadioGroupProps['validate']}
|
||||
{...(props as PartialRadioGroupProps)}
|
||||
@@ -211,7 +189,7 @@ export const Field = <T extends object>({
|
||||
|
||||
if (type === 'select') {
|
||||
return (
|
||||
<FieldWrapper {...props.wrapperProps}>
|
||||
<FieldWrapper>
|
||||
<Select
|
||||
validate={validate as unknown as SelectProps<T>['validate']}
|
||||
{...(props as PartialSelectProps<T>)}
|
||||
|
||||
@@ -109,6 +109,21 @@ export const buttonRecipe = cva({
|
||||
color: 'primaryDark.100 !important',
|
||||
},
|
||||
},
|
||||
secondaryDark: {
|
||||
backgroundColor: 'primaryDark.50',
|
||||
color: 'white',
|
||||
'&[data-pressed]': {
|
||||
backgroundColor: 'primaryDark.200',
|
||||
},
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: 'primaryDark.100',
|
||||
color: 'white',
|
||||
},
|
||||
'&[data-selected]': {
|
||||
backgroundColor: 'primaryDark.700 !important',
|
||||
color: 'primaryDark.100 !important',
|
||||
},
|
||||
},
|
||||
primaryTextDark: {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'white',
|
||||
|
||||
Reference in New Issue
Block a user