✨(frontend) add the AudioTab component
First draft, it lacks important features, as a visual indicator when the mic is active, and a trigger to test the audio output. I made some heuristics (e.g. default output/input, etc..) to ship a first version of this setting tabs that should work good enough to create value for our current users. Please refer to the inline comments.
This commit is contained in:
committed by
aleb_the_flash
parent
74b296aa37
commit
4d5aec9a49
@@ -1,5 +1,5 @@
|
|||||||
import { Dialog, type DialogProps } from '@/primitives'
|
import { Dialog, type DialogProps } from '@/primitives'
|
||||||
import { Tab, Tabs, TabPanel, TabList } from '@/primitives/Tabs.tsx'
|
import { Tab, Tabs, TabList } from '@/primitives/Tabs.tsx'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { text } from '@/primitives/Text.tsx'
|
import { text } from '@/primitives/Text.tsx'
|
||||||
import { Heading } from 'react-aria-components'
|
import { Heading } from 'react-aria-components'
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { AccountTab } from './tabs/AccountTab'
|
import { AccountTab } from './tabs/AccountTab'
|
||||||
import { GeneralTab } from '@/features/settings/components/tabs/GeneralTab.tsx'
|
import { GeneralTab } from '@/features/settings/components/tabs/GeneralTab.tsx'
|
||||||
|
import { AudioTab } from '@/features/settings/components/tabs/AudioTab.tsx'
|
||||||
|
|
||||||
const tabsStyle = css({
|
const tabsStyle = css({
|
||||||
maxHeight: '40.625rem', // fixme size copied from meet settings modal
|
maxHeight: '40.625rem', // fixme size copied from meet settings modal
|
||||||
@@ -68,9 +69,7 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={tabPanelContainerStyle}>
|
<div className={tabPanelContainerStyle}>
|
||||||
<AccountTab id="1" onOpenChange={props.onOpenChange} />
|
<AccountTab id="1" onOpenChange={props.onOpenChange} />
|
||||||
<TabPanel flex id="2">
|
<AudioTab id="2" />
|
||||||
There are your audio settings
|
|
||||||
</TabPanel>
|
|
||||||
<GeneralTab id="3" />
|
<GeneralTab id="3" />
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { DialogProps, Field, H } from '@/primitives'
|
||||||
|
|
||||||
|
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
|
||||||
|
import { useMediaDeviceSelect } from '@livekit/components-react'
|
||||||
|
import { isSafari } from '@/utils/livekit'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export type AudioTabProps = Pick<DialogProps, 'onOpenChange'> &
|
||||||
|
Pick<TabPanelProps, 'id'>
|
||||||
|
|
||||||
|
type DeviceItems = Array<{ value: string; label: string }>
|
||||||
|
|
||||||
|
export const AudioTab = ({ id }: AudioTabProps) => {
|
||||||
|
const { t } = useTranslation('settings')
|
||||||
|
|
||||||
|
const {
|
||||||
|
devices: devicesOut,
|
||||||
|
activeDeviceId: activeDeviceIdOut,
|
||||||
|
setActiveMediaDevice: setActiveMediaDeviceOut,
|
||||||
|
} = useMediaDeviceSelect({ kind: 'audiooutput' })
|
||||||
|
|
||||||
|
const {
|
||||||
|
devices: devicesIn,
|
||||||
|
activeDeviceId: activeDeviceIdIn,
|
||||||
|
setActiveMediaDevice: setActiveMediaDeviceIn,
|
||||||
|
} = useMediaDeviceSelect({ kind: 'audioinput' })
|
||||||
|
|
||||||
|
const itemsOut: DeviceItems = devicesOut.map((d) => ({
|
||||||
|
value: d.deviceId,
|
||||||
|
label: d.label,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const itemsIn: DeviceItems = devicesIn.map((d) => ({
|
||||||
|
value: d.deviceId,
|
||||||
|
label: d.label,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// The Permissions API is not fully supported in Firefox and Safari, and attempting to use it for microphone permissions
|
||||||
|
// may raise an error. As a workaround, we infer microphone permission status by checking if the list of audio input
|
||||||
|
// devices (devicesIn) is non-empty. If the list has one or more devices, we assume the user has granted microphone access.
|
||||||
|
const isMicEnabled = devicesIn?.length > 0
|
||||||
|
|
||||||
|
const disabledProps = isMicEnabled
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
placeholder: t('audio.permissionsRequired'),
|
||||||
|
isDisabled: true,
|
||||||
|
defaultSelectedKey: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
// No API to directly query the default audio device; this function heuristically finds it.
|
||||||
|
// Returns the item with value 'default' if present; otherwise, returns the first item in the list.
|
||||||
|
const getDefaultSelectedKey = (items: DeviceItems) => {
|
||||||
|
if (!items || items.length === 0) return
|
||||||
|
const defaultItem =
|
||||||
|
items.find((item) => item.value === 'default') || items[0]
|
||||||
|
return defaultItem.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabPanel padding={'md'} flex id={id}>
|
||||||
|
<H lvl={2}>{t('audio.microphone.heading')}</H>
|
||||||
|
<Field
|
||||||
|
type="select"
|
||||||
|
label={t('audio.microphone.label')}
|
||||||
|
items={itemsIn}
|
||||||
|
defaultSelectedKey={activeDeviceIdIn || getDefaultSelectedKey(itemsIn)}
|
||||||
|
onSelectionChange={(key) => setActiveMediaDeviceIn(key as string)}
|
||||||
|
{...disabledProps}
|
||||||
|
/>
|
||||||
|
{/* Safari has a known limitation where its implementation of 'enumerateDevices' does not include audio output devices.
|
||||||
|
To prevent errors or an empty selection list, we only render the speakers selection field on non-Safari browsers. */}
|
||||||
|
{!isSafari() && (
|
||||||
|
<>
|
||||||
|
<H lvl={2}>{t('audio.speakers.heading')}</H>
|
||||||
|
<Field
|
||||||
|
type="select"
|
||||||
|
label={t('audio.speakers.label')}
|
||||||
|
items={itemsOut}
|
||||||
|
defaultSelectedKey={
|
||||||
|
activeDeviceIdOut || getDefaultSelectedKey(itemsOut)
|
||||||
|
}
|
||||||
|
onSelectionChange={(key) => setActiveMediaDeviceOut(key as string)}
|
||||||
|
{...disabledProps}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TabPanel>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,6 +6,17 @@
|
|||||||
"nameLabel": "",
|
"nameLabel": "",
|
||||||
"authentication": ""
|
"authentication": ""
|
||||||
},
|
},
|
||||||
|
"audio": {
|
||||||
|
"microphone": {
|
||||||
|
"heading": "",
|
||||||
|
"label": ""
|
||||||
|
},
|
||||||
|
"speakers": {
|
||||||
|
"heading": "",
|
||||||
|
"label": ""
|
||||||
|
},
|
||||||
|
"permissionsRequired": ""
|
||||||
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"heading": ""
|
"heading": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,17 @@
|
|||||||
"nameLabel": "Votre Nom",
|
"nameLabel": "Votre Nom",
|
||||||
"authentication": "Authentication"
|
"authentication": "Authentication"
|
||||||
},
|
},
|
||||||
|
"audio": {
|
||||||
|
"microphone": {
|
||||||
|
"heading": "Microphone",
|
||||||
|
"label": "Select your audio input"
|
||||||
|
},
|
||||||
|
"speakers": {
|
||||||
|
"heading": "Speakers",
|
||||||
|
"label": "Select your audio output"
|
||||||
|
},
|
||||||
|
"permissionsRequired": "Permissions required"
|
||||||
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"heading": "Settings"
|
"heading": "Settings"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,17 @@
|
|||||||
"nameLabel": "Votre Nom",
|
"nameLabel": "Votre Nom",
|
||||||
"authentication": "Authentification"
|
"authentication": "Authentification"
|
||||||
},
|
},
|
||||||
|
"audio": {
|
||||||
|
"microphone": {
|
||||||
|
"heading": "Micro",
|
||||||
|
"label": "Sélectionner votre entrée audio"
|
||||||
|
},
|
||||||
|
"speakers": {
|
||||||
|
"heading": "Haut-parleurs",
|
||||||
|
"label": "Sélectionner votre sortie audio"
|
||||||
|
},
|
||||||
|
"permissionsRequired": "Autorisations nécessaires"
|
||||||
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"heading": "Paramètres"
|
"heading": "Paramètres"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ const StyledButton = styled(Button, {
|
|||||||
'&[data-pressed]': {
|
'&[data-pressed]': {
|
||||||
backgroundColor: 'control.hover',
|
backgroundColor: 'control.hover',
|
||||||
},
|
},
|
||||||
|
// fixme disabled style is being overridden by placeholder one and needs refinement.
|
||||||
|
'&[data-disabled]': {
|
||||||
|
color: 'default.subtle-text',
|
||||||
|
borderColor: 'gray.200',
|
||||||
|
boxShadow: '0 1px 2px rgba(0 0 0 / 0.02)',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user