🔒️(frontend) hide admin actions from unprivileged users in UI

Update interface to hide admin-only actions like participant muting from
users without room admin privileges, reflecting backend permission
restrictions implemented in previous commits.
This commit is contained in:
lebaudantoine
2025-08-27 15:45:02 +02:00
committed by aleb_the_flash
parent 4793f2fa8f
commit a85a62eb1a
5 changed files with 35 additions and 13 deletions

View File

@@ -21,6 +21,7 @@ import { useFullScreen } from '../hooks/useFullScreen'
import { Participant, Track } from 'livekit-client' import { Participant, Track } from 'livekit-client'
import { MuteAlertDialog } from './MuteAlertDialog' import { MuteAlertDialog } from './MuteAlertDialog'
import { useMuteParticipant } from '@/features/rooms/api/muteParticipant' import { useMuteParticipant } from '@/features/rooms/api/muteParticipant'
import { useCanMute } from '@/features/rooms/livekit/hooks/useCanMute'
const ZoomButton = ({ const ZoomButton = ({
trackRef, trackRef,
@@ -165,6 +166,8 @@ export const ParticipantTileFocus = ({
const isScreenShare = trackRef.source == Track.Source.ScreenShare const isScreenShare = trackRef.source == Track.Source.ScreenShare
const isLocal = trackRef.participant.isLocal const isLocal = trackRef.participant.isLocal
const canMute = useCanMute(participant)
return ( return (
<div <div
className={css({ className={css({
@@ -210,7 +213,7 @@ export const ParticipantTileFocus = ({
{participant.isLocal ? ( {participant.isLocal ? (
<EffectsButton /> <EffectsButton />
) : ( ) : (
<MuteButton participant={participant} /> canMute && <MuteButton participant={participant} />
)} )}
</> </>
) : ( ) : (

View File

@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'
import { Avatar } from '@/components/Avatar' import { Avatar } from '@/components/Avatar'
import { useLowerHandParticipant } from '@/features/rooms/api/lowerHandParticipant' import { useLowerHandParticipant } from '@/features/rooms/api/lowerHandParticipant'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor' import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import { Participant } from 'livekit-client' import { Participant } from 'livekit-client'
import { isLocal } from '@/utils/livekit' import { isLocal } from '@/utils/livekit'
import { RiHand } from '@remixicon/react' import { RiHand } from '@remixicon/react'
@@ -22,6 +23,7 @@ export const HandRaisedListItem = ({
const name = participant.name || participant.identity const name = participant.name || participant.identity
const { lowerHandParticipant } = useLowerHandParticipant() const { lowerHandParticipant } = useLowerHandParticipant()
const isAdminOrOwner = useIsAdminOrOwner()
return ( return (
<HStack <HStack
@@ -67,16 +69,18 @@ export const HandRaisedListItem = ({
)} )}
</Text> </Text>
</HStack> </HStack>
<Button {isAdminOrOwner && (
square <Button
variant="greyscale" square
size="sm" variant="greyscale"
onPress={() => lowerHandParticipant(participant)} size="sm"
tooltip={t('participants.lowerParticipantHand', { name })} onPress={() => lowerHandParticipant(participant)}
data-attr="participants-lower-hand" tooltip={t('participants.lowerParticipantHand', { name })}
> data-attr="participants-lower-hand"
<RiHand /> >
</Button> <RiHand />
</Button>
)}
</HStack> </HStack>
) )
} }

View File

@@ -2,6 +2,7 @@ import { Button } from '@/primitives'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Participant } from 'livekit-client' import { Participant } from 'livekit-client'
import { useLowerHandParticipants } from '@/features/rooms/api/lowerHandParticipants' import { useLowerHandParticipants } from '@/features/rooms/api/lowerHandParticipants'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
type LowerAllHandsButtonProps = { type LowerAllHandsButtonProps = {
participants: Array<Participant> participants: Array<Participant>
@@ -12,6 +13,10 @@ export const LowerAllHandsButton = ({
}: LowerAllHandsButtonProps) => { }: LowerAllHandsButtonProps) => {
const { lowerHandParticipants } = useLowerHandParticipants() const { lowerHandParticipants } = useLowerHandParticipants()
const { t } = useTranslation('rooms') const { t } = useTranslation('rooms')
const isAdminOrOwner = useIsAdminOrOwner()
if (!isAdminOrOwner) return null
return ( return (
<Button <Button
aria-label={t('participants.lowerParticipantsHand')} aria-label={t('participants.lowerParticipantsHand')}

View File

@@ -18,6 +18,7 @@ import { Button } from '@/primitives'
import { useState } from 'react' import { useState } from 'react'
import { MuteAlertDialog } from '../../MuteAlertDialog' import { MuteAlertDialog } from '../../MuteAlertDialog'
import { useMuteParticipant } from '@/features/rooms/api/muteParticipant' import { useMuteParticipant } from '@/features/rooms/api/muteParticipant'
import { useCanMute } from '@/features/rooms/livekit/hooks/useCanMute'
type MicIndicatorProps = { type MicIndicatorProps = {
participant: Participant participant: Participant
@@ -30,6 +31,8 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {
participant: participant, participant: participant,
source: Source.Microphone, source: Source.Microphone,
}) })
const canMute = useCanMute(participant)
const isSpeaking = useIsSpeaking(participant) const isSpeaking = useIsSpeaking(participant)
const [isAlertOpen, setIsAlertOpen] = useState(false) const [isAlertOpen, setIsAlertOpen] = useState(false)
const name = participant.name || participant.identity const name = participant.name || participant.identity
@@ -48,7 +51,7 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {
size="sm" size="sm"
tooltip={label} tooltip={label}
aria-label={label} aria-label={label}
isDisabled={isMuted} isDisabled={isMuted || !canMute}
onPress={() => onPress={() =>
!isMuted && isLocal(participant) !isMuted && isLocal(participant)
? muteParticipant(participant) ? muteParticipant(participant)
@@ -105,7 +108,7 @@ export const ParticipantListItem = ({
<Avatar name={name} bgColor={getParticipantColor(participant)} /> <Avatar name={name} bgColor={getParticipantColor(participant)} />
<VStack gap={0} alignItems="start"> <VStack gap={0} alignItems="start">
<Text <Text
sm variant="sm"
className={css({ className={css({
userSelect: 'none', userSelect: 'none',
cursor: 'default', cursor: 'default',

View File

@@ -0,0 +1,7 @@
import { useIsAdminOrOwner } from './useIsAdminOrOwner'
import { Participant } from 'livekit-client'
export const useCanMute = (participant: Participant) => {
const isAdminOrOwner = useIsAdminOrOwner()
return participant.isLocal || isAdminOrOwner
}