Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 80fdf3faf2 | |||
| 8d2df4001a | |||
| d610c0dff3 | |||
| bad054fa8c | |||
| 3b398ca3fd | |||
| 3c2c1a04db | |||
| cb30048b27 | |||
| 945279d504 | |||
| 13c9436dc3 |
@@ -5,7 +5,6 @@ import { Screen } from '@/layout/Screen'
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { LocalVideoTrack, Track } from 'livekit-client'
|
import { LocalVideoTrack, Track } from 'livekit-client'
|
||||||
import { H } from '@/primitives/H'
|
import { H } from '@/primitives/H'
|
||||||
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 { VStack } from '@/styled-system/jsx'
|
import { VStack } from '@/styled-system/jsx'
|
||||||
@@ -29,6 +28,9 @@ import { ApiAccessLevel } from '../api/ApiRoom'
|
|||||||
import { useLoginHint } from '@/hooks/useLoginHint'
|
import { useLoginHint } from '@/hooks/useLoginHint'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { openPermissionsDialog, permissionsStore } from '@/stores/permissions'
|
import { openPermissionsDialog, permissionsStore } from '@/stores/permissions'
|
||||||
|
import { ToggleDevice } from './join/ToggleDevice'
|
||||||
|
import { SelectDevice } from './join/SelectDevice'
|
||||||
|
import { useResolveDefaultDeviceId } from '../livekit/hooks/useResolveDefaultDevice'
|
||||||
|
|
||||||
const onError = (e: Error) => console.error('ERROR', e)
|
const onError = (e: Error) => console.error('ERROR', e)
|
||||||
|
|
||||||
@@ -139,6 +141,11 @@ export const Join = ({
|
|||||||
[tracks]
|
[tracks]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LiveKit by default populates device choices with "default" value.
|
||||||
|
// Instead, use the current device id used by the preview track as a default
|
||||||
|
useResolveDefaultDeviceId(audioDeviceId, audioTrack, saveAudioInputDeviceId)
|
||||||
|
useResolveDefaultDeviceId(videoDeviceId, videoTrack, saveVideoInputDeviceId)
|
||||||
|
|
||||||
const videoEl = useRef(null)
|
const videoEl = useRef(null)
|
||||||
const isVideoInitiated = useRef(false)
|
const isVideoInitiated = useRef(false)
|
||||||
|
|
||||||
@@ -160,7 +167,11 @@ export const Join = ({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
videoTrack?.detach()
|
videoTrack?.detach()
|
||||||
videoElement?.removeEventListener('loadedmetadata', handleVideoLoaded)
|
if (videoElement) {
|
||||||
|
videoElement.removeEventListener('loadedmetadata', handleVideoLoaded)
|
||||||
|
videoElement.style.opacity = '0'
|
||||||
|
}
|
||||||
|
isVideoInitiated.current = false
|
||||||
}
|
}
|
||||||
}, [videoTrack, videoEnabled])
|
}, [videoTrack, videoEnabled])
|
||||||
|
|
||||||
@@ -399,6 +410,28 @@ export const Join = ({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
height: '5rem',
|
||||||
|
width: '100%',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(to bottom, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.3) 40%, rgba(0, 0, 0, 0.1) 80%, rgba(0, 0, 0, 0) 100%)',
|
||||||
|
zIndex: 1,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
height: '5rem',
|
||||||
|
width: '100%',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.3) 35%, rgba(0, 0, 0, 0.1) 75%, rgba(0, 0, 0, 0) 100%)',
|
||||||
|
zIndex: 1,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -502,6 +535,33 @@ export const Join = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '1rem',
|
||||||
|
zIndex: '1',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '1rem',
|
||||||
|
justifyContent: 'center',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ToggleDevice
|
||||||
|
source={Track.Source.Microphone}
|
||||||
|
initialState={audioEnabled}
|
||||||
|
track={audioTrack}
|
||||||
|
onChange={(enabled) => saveAudioInputEnabled(enabled)}
|
||||||
|
onDeviceError={(error) => console.error(error)}
|
||||||
|
/>
|
||||||
|
<ToggleDevice
|
||||||
|
source={Track.Source.Camera}
|
||||||
|
initialState={videoEnabled}
|
||||||
|
track={videoTrack}
|
||||||
|
onChange={(enabled) => saveVideoInputEnabled(enabled)}
|
||||||
|
onDeviceError={(error) => console.error(error)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -528,32 +588,26 @@ export const Join = ({
|
|||||||
marginX: 'auto',
|
marginX: 'auto',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div
|
||||||
<SelectToggleDevice
|
className={css({
|
||||||
source={Track.Source.Microphone}
|
width: '50%',
|
||||||
initialState={audioEnabled}
|
})}
|
||||||
track={audioTrack}
|
>
|
||||||
initialDeviceId={audioDeviceId}
|
<SelectDevice
|
||||||
onChange={(enabled) => saveAudioInputEnabled(enabled)}
|
kind="audioinput"
|
||||||
onDeviceError={(error) => console.error(error)}
|
id={audioDeviceId}
|
||||||
onActiveDeviceChange={(deviceId) =>
|
onSubmit={saveAudioInputDeviceId}
|
||||||
saveAudioInputDeviceId(deviceId ?? '')
|
|
||||||
}
|
|
||||||
variant="tertiary"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div
|
||||||
<SelectToggleDevice
|
className={css({
|
||||||
source={Track.Source.Camera}
|
width: '50%',
|
||||||
initialState={videoEnabled}
|
})}
|
||||||
track={videoTrack}
|
>
|
||||||
initialDeviceId={videoDeviceId}
|
<SelectDevice
|
||||||
onChange={(enabled) => saveVideoInputEnabled(enabled)}
|
kind="videoinput"
|
||||||
onDeviceError={(error) => console.error(error)}
|
id={videoDeviceId}
|
||||||
onActiveDeviceChange={(deviceId) =>
|
onSubmit={saveVideoInputDeviceId}
|
||||||
saveVideoInputDeviceId(deviceId ?? '')
|
|
||||||
}
|
|
||||||
variant="tertiary"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import {
|
||||||
|
RemixiconComponentType,
|
||||||
|
RiMicLine,
|
||||||
|
RiVideoOnLine,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useMediaDeviceSelect } from '@livekit/components-react'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { Select } from '@/primitives/Select'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
import { permissionsStore } from '@/stores/permissions'
|
||||||
|
|
||||||
|
type DeviceItems = Array<{ value: string; label: string }>
|
||||||
|
|
||||||
|
type DeviceConfig = {
|
||||||
|
icon: RemixiconComponentType
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectDeviceProps = {
|
||||||
|
id?: string
|
||||||
|
onSubmit?: (id: string) => void
|
||||||
|
kind: MediaDeviceKind
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectDevicePermissionsProps = SelectDeviceProps & {
|
||||||
|
config: DeviceConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectDevicePermissions = ({
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
config,
|
||||||
|
onSubmit,
|
||||||
|
}: SelectDevicePermissionsProps) => {
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
|
||||||
|
const { devices, activeDeviceId, setActiveMediaDevice } =
|
||||||
|
useMediaDeviceSelect({ kind: kind, requestPermissions: true })
|
||||||
|
|
||||||
|
const items: DeviceItems = devices
|
||||||
|
.filter((d) => !!d.deviceId)
|
||||||
|
.map((d) => ({
|
||||||
|
value: d.deviceId,
|
||||||
|
label: d.label,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
aria-label={t(`${kind}.choose`)}
|
||||||
|
label=""
|
||||||
|
isDisabled={items.length === 0}
|
||||||
|
items={items}
|
||||||
|
iconComponent={config?.icon}
|
||||||
|
placeholder={t('selectDevice.loading')}
|
||||||
|
selectedKey={id || activeDeviceId}
|
||||||
|
onSelectionChange={(key) => {
|
||||||
|
onSubmit?.(key as string)
|
||||||
|
setActiveMediaDevice(key as string)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectDevice = ({ id, onSubmit, kind }: SelectDeviceProps) => {
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
|
||||||
|
const permissions = useSnapshot(permissionsStore)
|
||||||
|
|
||||||
|
const config = useMemo<DeviceConfig | undefined>(() => {
|
||||||
|
switch (kind) {
|
||||||
|
case 'audioinput':
|
||||||
|
return {
|
||||||
|
icon: RiMicLine,
|
||||||
|
}
|
||||||
|
case 'videoinput':
|
||||||
|
return {
|
||||||
|
icon: RiVideoOnLine,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [kind])
|
||||||
|
|
||||||
|
const isPermissionDeniedOrPrompted = useMemo(() => {
|
||||||
|
if (kind == 'audioinput') {
|
||||||
|
return permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
|
||||||
|
}
|
||||||
|
if (kind == 'videoinput') {
|
||||||
|
return permissions.isCameraDenied || permissions.isCameraPrompted
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, [kind, permissions])
|
||||||
|
|
||||||
|
if (!config) return null
|
||||||
|
|
||||||
|
if (isPermissionDeniedOrPrompted || permissions.isLoading) {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
aria-label={t(`${kind}.permissionsNeeded`)}
|
||||||
|
label=""
|
||||||
|
isDisabled={true}
|
||||||
|
items={[]}
|
||||||
|
iconComponent={config?.icon}
|
||||||
|
placeholder={t('selectDevice.permissionsNeeded')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectDevicePermissions
|
||||||
|
id={id}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
kind={kind}
|
||||||
|
config={config}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { UseTrackToggleProps } from '@livekit/components-react'
|
||||||
|
import { ToggleDevice as BaseToggleDevice } from '../../livekit/components/controls/ToggleDevice'
|
||||||
|
import {
|
||||||
|
TOGGLE_DEVICE_CONFIG,
|
||||||
|
ToggleSource,
|
||||||
|
} from '../../livekit/config/ToggleDeviceConfig'
|
||||||
|
import { LocalAudioTrack, LocalVideoTrack } from 'livekit-client'
|
||||||
|
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
||||||
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
import { permissionsStore } from '@/stores/permissions'
|
||||||
|
|
||||||
|
type ToggleDeviceProps<T extends ToggleSource> = UseTrackToggleProps<T> & {
|
||||||
|
track?: LocalAudioTrack | LocalVideoTrack
|
||||||
|
source: ToggleSource
|
||||||
|
variant?: NonNullable<ButtonRecipeProps>['variant']
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToggleDevice = <T extends ToggleSource>({
|
||||||
|
track,
|
||||||
|
onChange,
|
||||||
|
...props
|
||||||
|
}: ToggleDeviceProps<T>) => {
|
||||||
|
const config = TOGGLE_DEVICE_CONFIG[props.source]
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
throw new Error('Invalid source')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [isTrackEnabled, setIsTrackEnabled] = useState(
|
||||||
|
props.initialState ?? false
|
||||||
|
)
|
||||||
|
|
||||||
|
const permissions = useSnapshot(permissionsStore)
|
||||||
|
|
||||||
|
const isPermissionDeniedOrPrompted = useMemo(() => {
|
||||||
|
if (config.kind == 'audioinput') {
|
||||||
|
return permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
|
||||||
|
}
|
||||||
|
if (config.kind == 'videoinput') {
|
||||||
|
return permissions.isCameraDenied || permissions.isCameraPrompted
|
||||||
|
}
|
||||||
|
}, [config, permissions])
|
||||||
|
|
||||||
|
const toggle = useCallback(async () => {
|
||||||
|
if (!track) {
|
||||||
|
console.error('Track is undefined.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (isTrackEnabled) {
|
||||||
|
await track.mute()
|
||||||
|
setIsTrackEnabled(false)
|
||||||
|
onChange?.(false, true)
|
||||||
|
} else {
|
||||||
|
await track.unmute()
|
||||||
|
setIsTrackEnabled(true)
|
||||||
|
onChange?.(true, true)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to toggle track:', error)
|
||||||
|
}
|
||||||
|
}, [track, onChange, isTrackEnabled])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseToggleDevice
|
||||||
|
enabled={isTrackEnabled}
|
||||||
|
isPermissionDeniedOrPrompted={isPermissionDeniedOrPrompted}
|
||||||
|
toggle={toggle}
|
||||||
|
config={config}
|
||||||
|
variant="whiteCircle"
|
||||||
|
errorVariant="errorCircle"
|
||||||
|
toggleButtonProps={{
|
||||||
|
groupPosition: undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,14 +5,7 @@ import {
|
|||||||
UseTrackToggleProps,
|
UseTrackToggleProps,
|
||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
import { Button, Menu, MenuList } from '@/primitives'
|
import { Button, Menu, MenuList } from '@/primitives'
|
||||||
import {
|
import { RiArrowUpSLine } from '@remixicon/react'
|
||||||
RemixiconComponentType,
|
|
||||||
RiArrowDownSLine,
|
|
||||||
RiMicLine,
|
|
||||||
RiMicOffLine,
|
|
||||||
RiVideoOffLine,
|
|
||||||
RiVideoOnLine,
|
|
||||||
} from '@remixicon/react'
|
|
||||||
import {
|
import {
|
||||||
LocalAudioTrack,
|
LocalAudioTrack,
|
||||||
LocalVideoTrack,
|
LocalVideoTrack,
|
||||||
@@ -20,8 +13,6 @@ import {
|
|||||||
VideoCaptureOptions,
|
VideoCaptureOptions,
|
||||||
} from 'livekit-client'
|
} from 'livekit-client'
|
||||||
|
|
||||||
import { Shortcut } from '@/features/shortcuts/types'
|
|
||||||
|
|
||||||
import { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice.tsx'
|
import { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice.tsx'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
||||||
@@ -30,56 +21,17 @@ import { usePersistentUserChoices } from '../../hooks/usePersistentUserChoices'
|
|||||||
import { BackgroundProcessorFactory } from '../blur'
|
import { BackgroundProcessorFactory } from '../blur'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { permissionsStore } from '@/stores/permissions'
|
import { permissionsStore } from '@/stores/permissions'
|
||||||
|
import {
|
||||||
export type ToggleSource = Exclude<
|
TOGGLE_DEVICE_CONFIG,
|
||||||
Track.Source,
|
ToggleSource,
|
||||||
Track.Source.ScreenShareAudio | Track.Source.Unknown
|
} from '../../config/ToggleDeviceConfig'
|
||||||
>
|
|
||||||
|
|
||||||
type SelectToggleSource = Exclude<ToggleSource, Track.Source.ScreenShare>
|
|
||||||
|
|
||||||
export type SelectToggleDeviceConfig = {
|
|
||||||
kind: MediaDeviceKind
|
|
||||||
iconOn: RemixiconComponentType
|
|
||||||
iconOff: RemixiconComponentType
|
|
||||||
shortcut?: Shortcut
|
|
||||||
longPress?: Shortcut
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelectToggleDeviceConfigMap = {
|
|
||||||
[key in SelectToggleSource]: SelectToggleDeviceConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectToggleDeviceConfig: SelectToggleDeviceConfigMap = {
|
|
||||||
[Track.Source.Microphone]: {
|
|
||||||
kind: 'audioinput',
|
|
||||||
iconOn: RiMicLine,
|
|
||||||
iconOff: RiMicOffLine,
|
|
||||||
shortcut: {
|
|
||||||
key: 'd',
|
|
||||||
ctrlKey: true,
|
|
||||||
},
|
|
||||||
longPress: {
|
|
||||||
key: 'Space',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[Track.Source.Camera]: {
|
|
||||||
kind: 'videoinput',
|
|
||||||
iconOn: RiVideoOnLine,
|
|
||||||
iconOff: RiVideoOffLine,
|
|
||||||
shortcut: {
|
|
||||||
key: 'e',
|
|
||||||
ctrlKey: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelectToggleDeviceProps<T extends ToggleSource> =
|
type SelectToggleDeviceProps<T extends ToggleSource> =
|
||||||
UseTrackToggleProps<T> & {
|
UseTrackToggleProps<T> & {
|
||||||
track?: LocalAudioTrack | LocalVideoTrack | undefined
|
track?: LocalAudioTrack | LocalVideoTrack
|
||||||
initialDeviceId?: string
|
initialDeviceId?: string
|
||||||
onActiveDeviceChange: (deviceId: string) => void
|
onActiveDeviceChange: (deviceId: string) => void
|
||||||
source: SelectToggleSource
|
source: ToggleSource
|
||||||
variant?: NonNullable<ButtonRecipeProps>['variant']
|
variant?: NonNullable<ButtonRecipeProps>['variant']
|
||||||
menuVariant?: 'dark' | 'light'
|
menuVariant?: 'dark' | 'light'
|
||||||
hideMenu?: boolean
|
hideMenu?: boolean
|
||||||
@@ -94,7 +46,7 @@ export const SelectToggleDevice = <T extends ToggleSource>({
|
|||||||
menuVariant = 'light',
|
menuVariant = 'light',
|
||||||
...props
|
...props
|
||||||
}: SelectToggleDeviceProps<T>) => {
|
}: SelectToggleDeviceProps<T>) => {
|
||||||
const config = selectToggleDeviceConfig[props.source]
|
const config = TOGGLE_DEVICE_CONFIG[props.source]
|
||||||
if (!config) {
|
if (!config) {
|
||||||
throw new Error('Invalid source')
|
throw new Error('Invalid source')
|
||||||
}
|
}
|
||||||
@@ -198,7 +150,7 @@ export const SelectToggleDevice = <T extends ToggleSource>({
|
|||||||
: 'error2'
|
: 'error2'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<RiArrowDownSLine />
|
<RiArrowUpSLine />
|
||||||
</Button>
|
</Button>
|
||||||
<MenuList
|
<MenuList
|
||||||
items={devices.map((d) => ({
|
items={devices.map((d) => ({
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKey
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { appendShortcutLabel } from '@/features/shortcuts/utils'
|
import { appendShortcutLabel } from '@/features/shortcuts/utils'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { SelectToggleDeviceConfig } from './SelectToggleDevice'
|
|
||||||
import { PermissionNeededButton } from './PermissionNeededButton'
|
import { PermissionNeededButton } from './PermissionNeededButton'
|
||||||
import useLongPress from '@/features/shortcuts/useLongPress'
|
import useLongPress from '@/features/shortcuts/useLongPress'
|
||||||
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
|
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
|
||||||
@@ -15,13 +14,15 @@ import {
|
|||||||
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
||||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||||
import { openPermissionsDialog } from '@/stores/permissions'
|
import { openPermissionsDialog } from '@/stores/permissions'
|
||||||
|
import { ToggleDeviceConfig } from '../../config/ToggleDeviceConfig'
|
||||||
|
|
||||||
export type ToggleDeviceProps = {
|
export type ToggleDeviceProps = {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
isPermissionDeniedOrPrompted?: boolean
|
isPermissionDeniedOrPrompted?: boolean
|
||||||
toggle: () => void
|
toggle: () => void
|
||||||
config: SelectToggleDeviceConfig
|
config: ToggleDeviceConfig
|
||||||
variant?: NonNullable<ButtonRecipeProps>['variant']
|
variant?: NonNullable<ButtonRecipeProps>['variant']
|
||||||
|
errorVariant?: NonNullable<ButtonRecipeProps>['variant']
|
||||||
toggleButtonProps?: Partial<ToggleButtonProps>
|
toggleButtonProps?: Partial<ToggleButtonProps>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ export const ToggleDevice = ({
|
|||||||
enabled,
|
enabled,
|
||||||
toggle,
|
toggle,
|
||||||
variant = 'primaryDark',
|
variant = 'primaryDark',
|
||||||
|
errorVariant = 'error2',
|
||||||
toggleButtonProps,
|
toggleButtonProps,
|
||||||
isPermissionDeniedOrPrompted,
|
isPermissionDeniedOrPrompted,
|
||||||
}: ToggleDeviceProps) => {
|
}: ToggleDeviceProps) => {
|
||||||
@@ -72,7 +74,9 @@ export const ToggleDevice = ({
|
|||||||
{isPermissionDeniedOrPrompted && <PermissionNeededButton />}
|
{isPermissionDeniedOrPrompted && <PermissionNeededButton />}
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
isSelected={!enabled}
|
isSelected={!enabled}
|
||||||
variant={enabled && !isPermissionDeniedOrPrompted ? variant : 'error2'}
|
variant={
|
||||||
|
enabled && !isPermissionDeniedOrPrompted ? variant : errorVariant
|
||||||
|
}
|
||||||
shySelected
|
shySelected
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
isPermissionDeniedOrPrompted ? openPermissionsDialog() : toggle()
|
isPermissionDeniedOrPrompted ? openPermissionsDialog() : toggle()
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Track } from 'livekit-client'
|
||||||
|
import {
|
||||||
|
RemixiconComponentType,
|
||||||
|
RiMicLine,
|
||||||
|
RiMicOffLine,
|
||||||
|
RiVideoOffLine,
|
||||||
|
RiVideoOnLine,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { Shortcut } from '@/features/shortcuts/types'
|
||||||
|
|
||||||
|
export type ToggleSource = Exclude<
|
||||||
|
Track.Source,
|
||||||
|
| Track.Source.ScreenShareAudio
|
||||||
|
| Track.Source.Unknown
|
||||||
|
| Track.Source.ScreenShare
|
||||||
|
>
|
||||||
|
|
||||||
|
export type ToggleDeviceConfig = {
|
||||||
|
kind: MediaDeviceKind
|
||||||
|
iconOn: RemixiconComponentType
|
||||||
|
iconOff: RemixiconComponentType
|
||||||
|
shortcut?: Shortcut
|
||||||
|
longPress?: Shortcut
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToggleDeviceConfigMap = {
|
||||||
|
[key in ToggleSource]: ToggleDeviceConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TOGGLE_DEVICE_CONFIG = {
|
||||||
|
[Track.Source.Microphone]: {
|
||||||
|
kind: 'audioinput',
|
||||||
|
iconOn: RiMicLine,
|
||||||
|
iconOff: RiMicOffLine,
|
||||||
|
shortcut: {
|
||||||
|
key: 'd',
|
||||||
|
ctrlKey: true,
|
||||||
|
},
|
||||||
|
longPress: {
|
||||||
|
key: 'Space',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Track.Source.Camera]: {
|
||||||
|
kind: 'videoinput',
|
||||||
|
iconOn: RiVideoOnLine,
|
||||||
|
iconOff: RiVideoOffLine,
|
||||||
|
shortcut: {
|
||||||
|
key: 'e',
|
||||||
|
ctrlKey: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies ToggleDeviceConfigMap
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export const useResolveDefaultDeviceId = <
|
||||||
|
T extends { getDeviceId(): Promise<string | undefined> },
|
||||||
|
>(
|
||||||
|
currentId: string,
|
||||||
|
track: T | undefined,
|
||||||
|
save: (id: string) => void
|
||||||
|
) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentId !== 'default' || !track) return
|
||||||
|
const resolveDefaultDeviceId = async () => {
|
||||||
|
const actualDeviceId = await track.getDeviceId()
|
||||||
|
if (actualDeviceId && actualDeviceId !== 'default') save(actualDeviceId)
|
||||||
|
}
|
||||||
|
resolveDefaultDeviceId()
|
||||||
|
}, [currentId, track, save])
|
||||||
|
}
|
||||||
@@ -8,8 +8,13 @@
|
|||||||
"back": "Dem Meeting erneut beitreten"
|
"back": "Dem Meeting erneut beitreten"
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
|
"selectDevice": {
|
||||||
|
"loading": "Laden…",
|
||||||
|
"permissionsNeeded": "Genehmigung erforderlich"
|
||||||
|
},
|
||||||
"videoinput": {
|
"videoinput": {
|
||||||
"choose": "Kamera auswählen",
|
"choose": "Kamera auswählen",
|
||||||
|
"permissionsNeeded": "Kamera auswählen - genehmigung erforderlich",
|
||||||
"disable": "Kamera deaktivieren",
|
"disable": "Kamera deaktivieren",
|
||||||
"enable": "Kamera aktivieren",
|
"enable": "Kamera aktivieren",
|
||||||
"label": "Kamera",
|
"label": "Kamera",
|
||||||
@@ -17,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"audioinput": {
|
"audioinput": {
|
||||||
"choose": "Mikrofon auswählen",
|
"choose": "Mikrofon auswählen",
|
||||||
|
"permissionsNeeded": "Mikrofon auswählen - genehmigung erforderlich",
|
||||||
"disable": "Mikrofon deaktivieren",
|
"disable": "Mikrofon deaktivieren",
|
||||||
"enable": "Mikrofon aktivieren",
|
"enable": "Mikrofon aktivieren",
|
||||||
"label": "Mikrofon"
|
"label": "Mikrofon"
|
||||||
|
|||||||
@@ -8,8 +8,13 @@
|
|||||||
"back": "Rejoin the meeting"
|
"back": "Rejoin the meeting"
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
|
"selectDevice": {
|
||||||
|
"loading": "Loading…",
|
||||||
|
"permissionsNeeded": "Permission needed"
|
||||||
|
},
|
||||||
"videoinput": {
|
"videoinput": {
|
||||||
"choose": "Select camera",
|
"choose": "Select camera",
|
||||||
|
"permissionsNeeded": "Select camera - permission needed",
|
||||||
"disable": "Disable camera",
|
"disable": "Disable camera",
|
||||||
"enable": "Enable camera",
|
"enable": "Enable camera",
|
||||||
"label": "Camera",
|
"label": "Camera",
|
||||||
@@ -17,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"audioinput": {
|
"audioinput": {
|
||||||
"choose": "Select microphone",
|
"choose": "Select microphone",
|
||||||
|
"permissionsNeeded": "Select microphone - permission needed",
|
||||||
"disable": "Disable microphone",
|
"disable": "Disable microphone",
|
||||||
"enable": "Enable microphone",
|
"enable": "Enable microphone",
|
||||||
"label": "Microphone"
|
"label": "Microphone"
|
||||||
|
|||||||
@@ -8,8 +8,13 @@
|
|||||||
"back": "Réintégrer la réunion"
|
"back": "Réintégrer la réunion"
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
|
"selectDevice": {
|
||||||
|
"loading": "Chargement…",
|
||||||
|
"permissionsNeeded": "Autorisations nécessaires"
|
||||||
|
},
|
||||||
"videoinput": {
|
"videoinput": {
|
||||||
"choose": "Choisir la webcam",
|
"choose": "Choisir la webcam",
|
||||||
|
"permissionsNeeded": "Choisir la webcam - autorisations nécessaires",
|
||||||
"disable": "Désactiver la webcam",
|
"disable": "Désactiver la webcam",
|
||||||
"enable": "Activer la webcam",
|
"enable": "Activer la webcam",
|
||||||
"label": "Webcam",
|
"label": "Webcam",
|
||||||
@@ -17,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"audioinput": {
|
"audioinput": {
|
||||||
"choose": "Choisir le micro",
|
"choose": "Choisir le micro",
|
||||||
|
"permissionsNeeded": "Choisir le micro - autorisations nécessaires",
|
||||||
"disable": "Désactiver le micro",
|
"disable": "Désactiver le micro",
|
||||||
"enable": "Activer le micro",
|
"enable": "Activer le micro",
|
||||||
"label": "Microphone"
|
"label": "Microphone"
|
||||||
|
|||||||
@@ -8,8 +8,13 @@
|
|||||||
"back": "Sluit weer bij de vergadering aan"
|
"back": "Sluit weer bij de vergadering aan"
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
|
"selectDevice": {
|
||||||
|
"loading": "Bezig met laden…",
|
||||||
|
"permissionNeeded": "Toestemming vereist"
|
||||||
|
},
|
||||||
"videoinput": {
|
"videoinput": {
|
||||||
"choose": "Selecteer camera",
|
"choose": "Selecteer camera",
|
||||||
|
"permissionNeeded": "Selecteer camera - Toestemming vereist",
|
||||||
"disable": "Camera uitschakelen",
|
"disable": "Camera uitschakelen",
|
||||||
"enable": "Camera inschakelen",
|
"enable": "Camera inschakelen",
|
||||||
"label": "Camera",
|
"label": "Camera",
|
||||||
@@ -17,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"audioinput": {
|
"audioinput": {
|
||||||
"choose": "Selecteer microfoon",
|
"choose": "Selecteer microfoon",
|
||||||
|
"permissionNeeded": "Selecteer microfoon - Toestemming vereist",
|
||||||
"disable": "Microfoon dempen",
|
"disable": "Microfoon dempen",
|
||||||
"enable": "Microfoon dempen opheffen",
|
"enable": "Microfoon dempen opheffen",
|
||||||
"label": "Microfoon"
|
"label": "Microfoon"
|
||||||
|
|||||||
@@ -251,6 +251,20 @@ export const buttonRecipe = cva({
|
|||||||
color: 'error.300',
|
color: 'error.300',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
errorCircle: {
|
||||||
|
backgroundColor: 'error.500',
|
||||||
|
width: '56px',
|
||||||
|
height: '56px',
|
||||||
|
borderRadius: '100%',
|
||||||
|
color: 'white',
|
||||||
|
'&[data-hovered]': {
|
||||||
|
backgroundColor: 'error.600',
|
||||||
|
},
|
||||||
|
'&[data-pressed]': {
|
||||||
|
backgroundColor: 'error.700',
|
||||||
|
color: 'error.200',
|
||||||
|
},
|
||||||
|
},
|
||||||
// @TODO: better handling of colors… this is a mess
|
// @TODO: better handling of colors… this is a mess
|
||||||
success: {
|
success: {
|
||||||
colorPalette: 'success',
|
colorPalette: 'success',
|
||||||
|
|||||||
Reference in New Issue
Block a user