🚸(frontend) disable device controls when user lacks room permissions

Update user experience by clearly marking device toggle and control
components as disabled when users have insufficient room permissions.

Prevents confusion by providing visual feedback that device controls are
unavailable, improving clarity about what  actions users can perform
in their current role.
This commit is contained in:
lebaudantoine
2025-08-28 15:47:06 +02:00
committed by aleb_the_flash
parent 7d1f15ef91
commit 3d3242e148
7 changed files with 54 additions and 7 deletions
+1
View File
@@ -124,6 +124,7 @@ const config: Config = {
...pandaPreset.theme.tokens.colors,
primaryDark: {
50: { value: '#161622' },
75: { value: '#222234' },
100: { value: '#2D2D46' },
200: { value: '#43436A' },
300: { value: '#5A5A8F' },
@@ -7,12 +7,14 @@ import { Track } from 'livekit-client'
import { ToggleDevice } from './ToggleDevice'
import { css } from '@/styled-system/css'
import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices'
import { useCanPublishTrack } from '../../../hooks/useCanPublishTrack'
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
import Source = Track.Source
import * as React from 'react'
import { SelectDevice } from './SelectDevice'
import { SettingsButton } from './SettingsButton'
import { SettingsDialogExtendedKey } from '@/features/settings/type'
import { TrackSource } from '@livekit/protocol'
import Source = Track.Source
type AudioDevicesControlProps = Omit<
UseTrackToggleProps<Source.Microphone>,
@@ -50,6 +52,8 @@ export const AudioDevicesControl = ({
const cannotUseDevice = useCannotUseDevice(kind)
const selectLabel = t(`settings.${SettingsDialogExtendedKey.AUDIO}`)
const canPublishTrack = useCanPublishTrack(TrackSource.MICROPHONE)
return (
<div
className={css({
@@ -59,6 +63,7 @@ export const AudioDevicesControl = ({
>
<ToggleDevice
{...trackProps}
isDisabled={!canPublishTrack}
kind={kind}
toggle={trackProps.toggle as () => Promise<void>}
overrideToggleButtonProps={{
@@ -77,7 +82,9 @@ export const AudioDevicesControl = ({
groupPosition="right"
square
variant={
trackProps.enabled && !cannotUseDevice ? 'primaryDark' : 'error2'
!canPublishTrack || !trackProps.enabled || cannotUseDevice
? 'error2'
: 'primaryDark'
}
>
<RiArrowUpSLine />
@@ -27,6 +27,7 @@ type ToggleDeviceStyleProps = {
export type ToggleDeviceProps<T extends ToggleSource> = {
enabled: boolean
isDisabled?: boolean
toggle: (
forceState?: boolean,
captureOptions?: CaptureOptionsBySource<T>
@@ -39,6 +40,7 @@ export type ToggleDeviceProps<T extends ToggleSource> = {
export const ToggleDevice = <T extends ToggleSource>({
kind,
enabled,
isDisabled,
toggle,
context = 'room',
overrideToggleButtonProps,
@@ -105,7 +107,9 @@ export const ToggleDevice = <T extends ToggleSource>({
}, [enabled, kind, deviceShortcut, t])
const Icon =
enabled && !cannotUseDevice ? deviceIcons.toggleOn : deviceIcons.toggleOff
isDisabled || cannotUseDevice || !enabled
? deviceIcons.toggleOff
: deviceIcons.toggleOn
const roomContext = useMaybeRoomContext()
if (kind === 'audioinput' && pushToTalk && roomContext) {
@@ -117,7 +121,10 @@ export const ToggleDevice = <T extends ToggleSource>({
{cannotUseDevice && <PermissionNeededButton />}
<ToggleButton
isSelected={!enabled}
variant={enabled && !cannotUseDevice ? variant : errorVariant}
isDisabled={isDisabled}
variant={
isDisabled || cannotUseDevice || !enabled ? errorVariant : variant
}
shySelected
onPress={() => (cannotUseDevice ? openPermissionsDialog() : toggle())}
aria-label={toggleLabel}
@@ -7,6 +7,7 @@ import { Track, VideoCaptureOptions } from 'livekit-client'
import { ToggleDevice } from './ToggleDevice'
import { css } from '@/styled-system/css'
import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices'
import { useCanPublishTrack } from '../../../hooks/useCanPublishTrack'
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
import { useSidePanel } from '../../../hooks/useSidePanel'
import { BackgroundProcessorFactory } from '../../blur'
@@ -15,6 +16,7 @@ import * as React from 'react'
import { SelectDevice } from './SelectDevice'
import { SettingsButton } from './SettingsButton'
import { SettingsDialogExtendedKey } from '@/features/settings/type'
import { TrackSource } from '@livekit/protocol'
const EffectsButton = ({ onPress }: { onPress: () => void }) => {
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
@@ -95,6 +97,7 @@ export const VideoDeviceControl = ({
}
const selectLabel = t(`settings.${SettingsDialogExtendedKey.VIDEO}`)
const canPublishTrack = useCanPublishTrack(TrackSource.CAMERA)
return (
<div
@@ -105,6 +108,7 @@ export const VideoDeviceControl = ({
>
<ToggleDevice
{...trackProps}
isDisabled={!canPublishTrack}
kind={kind}
toggle={toggleWithProcessor}
overrideToggleButtonProps={{
@@ -123,7 +127,9 @@ export const VideoDeviceControl = ({
groupPosition="right"
square
variant={
trackProps.enabled && !cannotUseDevice ? 'primaryDark' : 'error2'
!canPublishTrack || !trackProps.enabled || cannotUseDevice
? 'error2'
: 'primaryDark'
}
>
<RiArrowUpSLine />
@@ -6,6 +6,8 @@ import { Track } from 'livekit-client'
import React from 'react'
import { type ButtonRecipeProps } from '@/primitives/buttonRecipe'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
import { TrackSource } from '@livekit/protocol'
import { useCanPublishTrack } from '@/features/rooms/livekit/hooks/useCanPublishTrack'
type Props = Omit<
UseTrackToggleProps<Track.Source.ScreenShare>,
@@ -29,10 +31,13 @@ export const ScreenShareToggle = ({
const tooltipLabel = enabled ? 'stop' : 'start'
const Icon = enabled ? RiCloseFill : RiArrowUpLine
const canShareScreen = useCanPublishTrack(TrackSource.SCREEN_SHARE)
// fixme - remove ToggleButton custom styles when we design a proper icon
return (
<ToggleButton
isSelected={enabled}
isDisabled={!canShareScreen}
square
variant={variant}
tooltip={t(tooltipLabel)}
@@ -0,0 +1,17 @@
import { TrackSource } from '@livekit/protocol'
import {
useLocalParticipant,
useParticipantPermissions,
} from '@livekit/components-react'
export function useCanPublishTrack(trackSource: TrackSource): boolean {
const { localParticipant } = useLocalParticipant()
const permissions = useParticipantPermissions({
participant: localParticipant,
})
return Boolean(
permissions?.canPublish &&
permissions?.canPublishSources?.includes(trackSource)
)
}
+6 -2
View File
@@ -148,6 +148,10 @@ export const buttonRecipe = cva({
backgroundColor: 'primaryDark.900',
color: 'primaryDark.100',
},
'&[data-disabled]': {
backgroundColor: 'primaryDark.75',
color: 'primaryDark.300',
},
'&[data-hovered]': {
backgroundColor: 'primaryDark.300',
color: 'white',
@@ -247,8 +251,8 @@ export const buttonRecipe = cva({
color: 'error.100 !important',
},
'&[data-disabled]': {
backgroundColor: 'error.200',
color: 'error.300',
backgroundColor: 'error.200 !important',
color: 'error.300 !important',
},
},
errorCircle: {