From a83c01f0b68bf1ac179752ec8b2b8cc21d82287a Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 28 Jul 2026 10:02:39 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20allow=20promoting=20authe?= =?UTF-8?q?nticated=20participants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a new feature that lets a user promote one of the authenticated participants of the meeting to a role with additional privileges. Known limitations: * Only authenticated participants can be promoted, but there is no visual indicator yet distinguishing authenticated from anonymous participants. This will be added in a follow-up commit. * The resource_access data fetched in the initial API call becomes stale after a promotion. It is not currently used in the product, so this is not visible, but it should either be refreshed later or removed from the initial fetch. * Demoting a promoted user turns them into a member, which is still a privileged role. This is a deliberate choice until we introduce finer-grained tuning of participant roles. --- .../components/ParticipantRow.tsx | 12 ++++- .../components/menu/ParticipantMenu.tsx | 32 +++++++++++-- .../components/menu/ParticipantMenuButton.tsx | 3 -- .../components/menu/items/PromoteMenuItem.tsx | 47 +++++++++++++++++++ .../components/menu/items/RemoveMenuItem.tsx | 47 +++++++++++-------- .../features/rooms/api/removeParticipant.ts | 5 +- .../utils/getParticipantIsRoomAdminOrOwner.ts | 3 ++ src/frontend/src/locales/de/rooms.json | 10 ++++ src/frontend/src/locales/en/rooms.json | 10 ++++ src/frontend/src/locales/fr/rooms.json | 10 ++++ src/frontend/src/locales/nl/rooms.json | 10 ++++ 11 files changed, 157 insertions(+), 32 deletions(-) create mode 100644 src/frontend/src/features/participants/components/menu/items/PromoteMenuItem.tsx diff --git a/src/frontend/src/features/participants/components/ParticipantRow.tsx b/src/frontend/src/features/participants/components/ParticipantRow.tsx index a5ddd2a5..f3aee30a 100644 --- a/src/frontend/src/features/participants/components/ParticipantRow.tsx +++ b/src/frontend/src/features/participants/components/ParticipantRow.tsx @@ -5,7 +5,11 @@ import { Text } from '@/primitives/Text' import { useTranslation } from 'react-i18next' import { Avatar } from '@/components/Avatar' import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor' -import { getParticipantIsRoomOwner } from '@/features/rooms/utils/getParticipantIsRoomAdminOrOwner' +import { + getParticipantIsRoomAdmin, + getParticipantIsRoomOwner, + getParticipantIsRoomMember, +} from '@/features/rooms/utils/getParticipantIsRoomAdminOrOwner' import { type LocalParticipant, type Participant, Track } from 'livekit-client' import { isLocal } from '@/utils/livekit' import { @@ -149,6 +153,12 @@ export const ParticipantRow = ({ participant }: ParticipantListItemProps) => { {getParticipantIsRoomOwner(participant) && ( {t('participants.host')} )} + {getParticipantIsRoomAdmin(participant) && ( + {t('participants.cohost')} + )} + {getParticipantIsRoomMember(participant) && ( + {t('participants.member')} + )} diff --git a/src/frontend/src/features/participants/components/menu/ParticipantMenu.tsx b/src/frontend/src/features/participants/components/menu/ParticipantMenu.tsx index 2cc6397e..bdba8cbd 100644 --- a/src/frontend/src/features/participants/components/menu/ParticipantMenu.tsx +++ b/src/frontend/src/features/participants/components/menu/ParticipantMenu.tsx @@ -1,24 +1,46 @@ import { Menu as RACMenu } from 'react-aria-components' import type { Participant } from 'livekit-client' -import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner' import { PinMenuItem } from './items/PinMenuItem' import { RemoveMenuItem } from './items/RemoveMenuItem' +import { PromoteMenuItem } from './items/PromoteMenuItem' +import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner' +import { useParticipantAttributes } from '@livekit/components-react' export const ParticipantMenu = ({ participant, }: { participant: Participant }) => { - const isAdminOrOwner = useIsAdminOrOwner() - const canModerateParticipant = !participant.isLocal && isAdminOrOwner + const isLocalUserAdminOrOwner = useIsAdminOrOwner() + + const { attributes } = useParticipantAttributes({ participant }) + + const isAdmin = attributes?.room_role === 'administrator' + const isOwner = attributes?.room_role === 'owner' + const isAuthenticated = attributes?.is_authenticated == 'true' + + const canManage = !participant.isLocal && isLocalUserAdminOrOwner && !isOwner + return ( - {canModerateParticipant && } + {canManage && ( + + )} + {canManage && isAuthenticated && ( + + )} ) } diff --git a/src/frontend/src/features/participants/components/menu/ParticipantMenuButton.tsx b/src/frontend/src/features/participants/components/menu/ParticipantMenuButton.tsx index 900fd2a8..197b1244 100644 --- a/src/frontend/src/features/participants/components/menu/ParticipantMenuButton.tsx +++ b/src/frontend/src/features/participants/components/menu/ParticipantMenuButton.tsx @@ -1,7 +1,6 @@ import { Button, Menu } from '@/primitives' import { RiMore2Fill } from '@remixicon/react' import { ParticipantMenu } from './ParticipantMenu' -import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner' import type { Participant } from 'livekit-client' import { useTranslation } from 'react-i18next' @@ -11,8 +10,6 @@ export const ParticipantMenuButton = ({ participant: Participant }) => { const { t } = useTranslation('rooms', { keyPrefix: 'participants' }) - const isAdminOrOwner = useIsAdminOrOwner() - if (!isAdminOrOwner) return null return (