🚸(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:
committed by
aleb_the_flash
parent
7d1f15ef91
commit
3d3242e148
@@ -124,6 +124,7 @@ const config: Config = {
|
|||||||
...pandaPreset.theme.tokens.colors,
|
...pandaPreset.theme.tokens.colors,
|
||||||
primaryDark: {
|
primaryDark: {
|
||||||
50: { value: '#161622' },
|
50: { value: '#161622' },
|
||||||
|
75: { value: '#222234' },
|
||||||
100: { value: '#2D2D46' },
|
100: { value: '#2D2D46' },
|
||||||
200: { value: '#43436A' },
|
200: { value: '#43436A' },
|
||||||
300: { value: '#5A5A8F' },
|
300: { value: '#5A5A8F' },
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import { Track } from 'livekit-client'
|
|||||||
import { ToggleDevice } from './ToggleDevice'
|
import { ToggleDevice } from './ToggleDevice'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices'
|
import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices'
|
||||||
|
import { useCanPublishTrack } from '../../../hooks/useCanPublishTrack'
|
||||||
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
||||||
import Source = Track.Source
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { SelectDevice } from './SelectDevice'
|
import { SelectDevice } from './SelectDevice'
|
||||||
import { SettingsButton } from './SettingsButton'
|
import { SettingsButton } from './SettingsButton'
|
||||||
import { SettingsDialogExtendedKey } from '@/features/settings/type'
|
import { SettingsDialogExtendedKey } from '@/features/settings/type'
|
||||||
|
import { TrackSource } from '@livekit/protocol'
|
||||||
|
import Source = Track.Source
|
||||||
|
|
||||||
type AudioDevicesControlProps = Omit<
|
type AudioDevicesControlProps = Omit<
|
||||||
UseTrackToggleProps<Source.Microphone>,
|
UseTrackToggleProps<Source.Microphone>,
|
||||||
@@ -50,6 +52,8 @@ export const AudioDevicesControl = ({
|
|||||||
const cannotUseDevice = useCannotUseDevice(kind)
|
const cannotUseDevice = useCannotUseDevice(kind)
|
||||||
const selectLabel = t(`settings.${SettingsDialogExtendedKey.AUDIO}`)
|
const selectLabel = t(`settings.${SettingsDialogExtendedKey.AUDIO}`)
|
||||||
|
|
||||||
|
const canPublishTrack = useCanPublishTrack(TrackSource.MICROPHONE)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
@@ -59,6 +63,7 @@ export const AudioDevicesControl = ({
|
|||||||
>
|
>
|
||||||
<ToggleDevice
|
<ToggleDevice
|
||||||
{...trackProps}
|
{...trackProps}
|
||||||
|
isDisabled={!canPublishTrack}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
toggle={trackProps.toggle as () => Promise<void>}
|
toggle={trackProps.toggle as () => Promise<void>}
|
||||||
overrideToggleButtonProps={{
|
overrideToggleButtonProps={{
|
||||||
@@ -77,7 +82,9 @@ export const AudioDevicesControl = ({
|
|||||||
groupPosition="right"
|
groupPosition="right"
|
||||||
square
|
square
|
||||||
variant={
|
variant={
|
||||||
trackProps.enabled && !cannotUseDevice ? 'primaryDark' : 'error2'
|
!canPublishTrack || !trackProps.enabled || cannotUseDevice
|
||||||
|
? 'error2'
|
||||||
|
: 'primaryDark'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<RiArrowUpSLine />
|
<RiArrowUpSLine />
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type ToggleDeviceStyleProps = {
|
|||||||
|
|
||||||
export type ToggleDeviceProps<T extends ToggleSource> = {
|
export type ToggleDeviceProps<T extends ToggleSource> = {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
isDisabled?: boolean
|
||||||
toggle: (
|
toggle: (
|
||||||
forceState?: boolean,
|
forceState?: boolean,
|
||||||
captureOptions?: CaptureOptionsBySource<T>
|
captureOptions?: CaptureOptionsBySource<T>
|
||||||
@@ -39,6 +40,7 @@ export type ToggleDeviceProps<T extends ToggleSource> = {
|
|||||||
export const ToggleDevice = <T extends ToggleSource>({
|
export const ToggleDevice = <T extends ToggleSource>({
|
||||||
kind,
|
kind,
|
||||||
enabled,
|
enabled,
|
||||||
|
isDisabled,
|
||||||
toggle,
|
toggle,
|
||||||
context = 'room',
|
context = 'room',
|
||||||
overrideToggleButtonProps,
|
overrideToggleButtonProps,
|
||||||
@@ -105,7 +107,9 @@ export const ToggleDevice = <T extends ToggleSource>({
|
|||||||
}, [enabled, kind, deviceShortcut, t])
|
}, [enabled, kind, deviceShortcut, t])
|
||||||
|
|
||||||
const Icon =
|
const Icon =
|
||||||
enabled && !cannotUseDevice ? deviceIcons.toggleOn : deviceIcons.toggleOff
|
isDisabled || cannotUseDevice || !enabled
|
||||||
|
? deviceIcons.toggleOff
|
||||||
|
: deviceIcons.toggleOn
|
||||||
|
|
||||||
const roomContext = useMaybeRoomContext()
|
const roomContext = useMaybeRoomContext()
|
||||||
if (kind === 'audioinput' && pushToTalk && roomContext) {
|
if (kind === 'audioinput' && pushToTalk && roomContext) {
|
||||||
@@ -117,7 +121,10 @@ export const ToggleDevice = <T extends ToggleSource>({
|
|||||||
{cannotUseDevice && <PermissionNeededButton />}
|
{cannotUseDevice && <PermissionNeededButton />}
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
isSelected={!enabled}
|
isSelected={!enabled}
|
||||||
variant={enabled && !cannotUseDevice ? variant : errorVariant}
|
isDisabled={isDisabled}
|
||||||
|
variant={
|
||||||
|
isDisabled || cannotUseDevice || !enabled ? errorVariant : variant
|
||||||
|
}
|
||||||
shySelected
|
shySelected
|
||||||
onPress={() => (cannotUseDevice ? openPermissionsDialog() : toggle())}
|
onPress={() => (cannotUseDevice ? openPermissionsDialog() : toggle())}
|
||||||
aria-label={toggleLabel}
|
aria-label={toggleLabel}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Track, VideoCaptureOptions } from 'livekit-client'
|
|||||||
import { ToggleDevice } from './ToggleDevice'
|
import { ToggleDevice } from './ToggleDevice'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices'
|
import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices'
|
||||||
|
import { useCanPublishTrack } from '../../../hooks/useCanPublishTrack'
|
||||||
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
||||||
import { useSidePanel } from '../../../hooks/useSidePanel'
|
import { useSidePanel } from '../../../hooks/useSidePanel'
|
||||||
import { BackgroundProcessorFactory } from '../../blur'
|
import { BackgroundProcessorFactory } from '../../blur'
|
||||||
@@ -15,6 +16,7 @@ import * as React from 'react'
|
|||||||
import { SelectDevice } from './SelectDevice'
|
import { SelectDevice } from './SelectDevice'
|
||||||
import { SettingsButton } from './SettingsButton'
|
import { SettingsButton } from './SettingsButton'
|
||||||
import { SettingsDialogExtendedKey } from '@/features/settings/type'
|
import { SettingsDialogExtendedKey } from '@/features/settings/type'
|
||||||
|
import { TrackSource } from '@livekit/protocol'
|
||||||
|
|
||||||
const EffectsButton = ({ onPress }: { onPress: () => void }) => {
|
const EffectsButton = ({ onPress }: { onPress: () => void }) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
|
||||||
@@ -95,6 +97,7 @@ export const VideoDeviceControl = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectLabel = t(`settings.${SettingsDialogExtendedKey.VIDEO}`)
|
const selectLabel = t(`settings.${SettingsDialogExtendedKey.VIDEO}`)
|
||||||
|
const canPublishTrack = useCanPublishTrack(TrackSource.CAMERA)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -105,6 +108,7 @@ export const VideoDeviceControl = ({
|
|||||||
>
|
>
|
||||||
<ToggleDevice
|
<ToggleDevice
|
||||||
{...trackProps}
|
{...trackProps}
|
||||||
|
isDisabled={!canPublishTrack}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
toggle={toggleWithProcessor}
|
toggle={toggleWithProcessor}
|
||||||
overrideToggleButtonProps={{
|
overrideToggleButtonProps={{
|
||||||
@@ -123,7 +127,9 @@ export const VideoDeviceControl = ({
|
|||||||
groupPosition="right"
|
groupPosition="right"
|
||||||
square
|
square
|
||||||
variant={
|
variant={
|
||||||
trackProps.enabled && !cannotUseDevice ? 'primaryDark' : 'error2'
|
!canPublishTrack || !trackProps.enabled || cannotUseDevice
|
||||||
|
? 'error2'
|
||||||
|
: 'primaryDark'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<RiArrowUpSLine />
|
<RiArrowUpSLine />
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { Track } from 'livekit-client'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { type ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
import { type ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
||||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||||
|
import { TrackSource } from '@livekit/protocol'
|
||||||
|
import { useCanPublishTrack } from '@/features/rooms/livekit/hooks/useCanPublishTrack'
|
||||||
|
|
||||||
type Props = Omit<
|
type Props = Omit<
|
||||||
UseTrackToggleProps<Track.Source.ScreenShare>,
|
UseTrackToggleProps<Track.Source.ScreenShare>,
|
||||||
@@ -29,10 +31,13 @@ export const ScreenShareToggle = ({
|
|||||||
const tooltipLabel = enabled ? 'stop' : 'start'
|
const tooltipLabel = enabled ? 'stop' : 'start'
|
||||||
const Icon = enabled ? RiCloseFill : RiArrowUpLine
|
const Icon = enabled ? RiCloseFill : RiArrowUpLine
|
||||||
|
|
||||||
|
const canShareScreen = useCanPublishTrack(TrackSource.SCREEN_SHARE)
|
||||||
|
|
||||||
// fixme - remove ToggleButton custom styles when we design a proper icon
|
// fixme - remove ToggleButton custom styles when we design a proper icon
|
||||||
return (
|
return (
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
isSelected={enabled}
|
isSelected={enabled}
|
||||||
|
isDisabled={!canShareScreen}
|
||||||
square
|
square
|
||||||
variant={variant}
|
variant={variant}
|
||||||
tooltip={t(tooltipLabel)}
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -148,6 +148,10 @@ export const buttonRecipe = cva({
|
|||||||
backgroundColor: 'primaryDark.900',
|
backgroundColor: 'primaryDark.900',
|
||||||
color: 'primaryDark.100',
|
color: 'primaryDark.100',
|
||||||
},
|
},
|
||||||
|
'&[data-disabled]': {
|
||||||
|
backgroundColor: 'primaryDark.75',
|
||||||
|
color: 'primaryDark.300',
|
||||||
|
},
|
||||||
'&[data-hovered]': {
|
'&[data-hovered]': {
|
||||||
backgroundColor: 'primaryDark.300',
|
backgroundColor: 'primaryDark.300',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
@@ -247,8 +251,8 @@ export const buttonRecipe = cva({
|
|||||||
color: 'error.100 !important',
|
color: 'error.100 !important',
|
||||||
},
|
},
|
||||||
'&[data-disabled]': {
|
'&[data-disabled]': {
|
||||||
backgroundColor: 'error.200',
|
backgroundColor: 'error.200 !important',
|
||||||
color: 'error.300',
|
color: 'error.300 !important',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errorCircle: {
|
errorCircle: {
|
||||||
|
|||||||
Reference in New Issue
Block a user