mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-29 05:09:16 +00:00
✨(frontend) allow promoting authenticated participants
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.
This commit is contained in:
@@ -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) && (
|
||||
<Text variant="xsNote">{t('participants.host')}</Text>
|
||||
)}
|
||||
{getParticipantIsRoomAdmin(participant) && (
|
||||
<Text variant="xsNote">{t('participants.cohost')}</Text>
|
||||
)}
|
||||
{getParticipantIsRoomMember(participant) && (
|
||||
<Text variant="xsNote">{t('participants.member')}</Text>
|
||||
)}
|
||||
</VStack>
|
||||
</HStack>
|
||||
<HStack>
|
||||
|
||||
@@ -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 (
|
||||
<RACMenu
|
||||
style={{
|
||||
minWidth: '75px',
|
||||
minWidth: '100px',
|
||||
}}
|
||||
>
|
||||
<PinMenuItem participant={participant} />
|
||||
{canModerateParticipant && <RemoveMenuItem participant={participant} />}
|
||||
{canManage && (
|
||||
<RemoveMenuItem
|
||||
identity={participant.identity}
|
||||
displayedName={participant.name}
|
||||
/>
|
||||
)}
|
||||
{canManage && isAuthenticated && (
|
||||
<PromoteMenuItem
|
||||
identity={participant.identity}
|
||||
isAdmin={isAdmin}
|
||||
displayedName={participant.name}
|
||||
/>
|
||||
)}
|
||||
</RACMenu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Menu>
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { MenuItem } from 'react-aria-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiAdminLine, RiUserMinusLine } from '@remixicon/react'
|
||||
import { useParticipantRole } from '@/features/participants/api/updateParticipantRole'
|
||||
import { menuRecipe } from '@/primitives/menuRecipe'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
|
||||
type PromoteMenuItemProps = {
|
||||
identity: string
|
||||
displayedName?: string
|
||||
isAdmin: boolean
|
||||
}
|
||||
|
||||
export const PromoteMenuItem = React.memo(
|
||||
({ identity, displayedName, isAdmin }: PromoteMenuItemProps) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'participantMenu' })
|
||||
|
||||
const { updateParticipantRole } = useParticipantRole()
|
||||
|
||||
const label = isAdmin ? 'demote' : 'promote'
|
||||
const Icon = isAdmin ? RiUserMinusLine : RiAdminLine
|
||||
|
||||
const toggleRole = useCallback(
|
||||
() =>
|
||||
updateParticipantRole(identity, isAdmin ? 'member' : 'administrator'),
|
||||
[isAdmin, updateParticipantRole, identity]
|
||||
)
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
aria-label={t(`${label}.ariaLabel`, {
|
||||
name: displayedName || identity,
|
||||
})}
|
||||
className={menuRecipe({ icon: true }).item}
|
||||
onAction={toggleRole}
|
||||
>
|
||||
<HStack gap={0.25} minWidth={280}>
|
||||
<Icon size={20} aria-hidden />
|
||||
{t(`${label}.label`)}
|
||||
</HStack>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
PromoteMenuItem.displayName = 'PromoteMenuItem'
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Participant } from 'livekit-client'
|
||||
import React from 'react'
|
||||
import { menuRecipe } from '@/primitives/menuRecipe'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
@@ -6,23 +6,30 @@ import { MenuItem } from 'react-aria-components'
|
||||
import { useRemoveParticipant } from '@/features/rooms/api/removeParticipant'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const RemoveMenuItem = ({
|
||||
participant,
|
||||
}: {
|
||||
participant: Participant
|
||||
}) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'participantMenu.remove' })
|
||||
const { removeParticipant } = useRemoveParticipant()
|
||||
return (
|
||||
<MenuItem
|
||||
aria-label={t('ariaLabel', { name: participant.name })}
|
||||
className={menuRecipe({ icon: true }).item}
|
||||
onAction={() => removeParticipant(participant)}
|
||||
>
|
||||
<HStack gap={0.25}>
|
||||
<RiCloseLine size={20} aria-hidden />
|
||||
{t('label')}
|
||||
</HStack>
|
||||
</MenuItem>
|
||||
)
|
||||
type RemoveMenuItemProps = {
|
||||
identity: string
|
||||
displayedName?: string
|
||||
}
|
||||
|
||||
export const RemoveMenuItem = React.memo(
|
||||
({ identity, displayedName }: RemoveMenuItemProps) => {
|
||||
const { t } = useTranslation('rooms', {
|
||||
keyPrefix: 'participantMenu.remove',
|
||||
})
|
||||
const { removeParticipant } = useRemoveParticipant()
|
||||
return (
|
||||
<MenuItem
|
||||
aria-label={t('ariaLabel', { name: displayedName || identity })}
|
||||
className={menuRecipe({ icon: true }).item}
|
||||
onAction={() => removeParticipant(identity)}
|
||||
>
|
||||
<HStack gap={0.25}>
|
||||
<RiCloseLine size={20} aria-hidden />
|
||||
{t('label')}
|
||||
</HStack>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
RemoveMenuItem.displayName = 'RemoveMenuItem'
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { Participant } from 'livekit-client'
|
||||
import { useRoomData } from '../livekit/hooks/useRoomData'
|
||||
import { fetchApi } from '@/api/fetchApi'
|
||||
|
||||
export const useRemoveParticipant = () => {
|
||||
const data = useRoomData()
|
||||
|
||||
const removeParticipant = async (participant: Participant) => {
|
||||
const removeParticipant = async (identity: string) => {
|
||||
if (!data?.id) {
|
||||
throw new Error('Room id is not available')
|
||||
}
|
||||
@@ -13,7 +12,7 @@ export const useRemoveParticipant = () => {
|
||||
return fetchApi(`rooms/${data.id}/remove-participant/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
participant_identity: participant.identity,
|
||||
participant_identity: identity,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ export const getParticipantIsRoomAdmin = (participant: Participant): boolean =>
|
||||
export const getParticipantIsRoomOwner = (participant: Participant): boolean =>
|
||||
participantHasRoomRole(participant, ['owner'])
|
||||
|
||||
export const getParticipantIsRoomMember = (participant: Participant): boolean =>
|
||||
participantHasRoomRole(participant, ['member'])
|
||||
|
||||
export const getParticipantIsRoomAdminOrOwner = (
|
||||
participant: Participant
|
||||
): boolean => participantHasRoomRole(participant, ['administrator', 'owner'])
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"you": "Du",
|
||||
"unknown": "Unbekannte Person",
|
||||
"host": "Host",
|
||||
"cohost": "Co-host",
|
||||
"member": "Mitglied",
|
||||
"contributors": "Teilnehmende",
|
||||
"collapsable": {
|
||||
"open": "{{name}}-Liste öffnen",
|
||||
@@ -627,6 +629,14 @@
|
||||
"pin": {
|
||||
"label": "Anheften",
|
||||
"ariaLabel": "Hefte {{name}} an"
|
||||
},
|
||||
"promote": {
|
||||
"label": "Zum Co-Host machen",
|
||||
"ariaLabel": "{{name}} zum Co-Host machen"
|
||||
},
|
||||
"demote": {
|
||||
"label": "Co-Host-Rolle entfernen",
|
||||
"ariaLabel": "Die Co-Host-Rolle von {{name}} entfernen"
|
||||
}
|
||||
},
|
||||
"pinAnnouncements": {
|
||||
|
||||
@@ -582,6 +582,8 @@
|
||||
"you": "You",
|
||||
"unknown": "Unknown participant",
|
||||
"host": "Host",
|
||||
"cohost": "Co-host",
|
||||
"member": "Member",
|
||||
"contributors": "Contributors",
|
||||
"collapsable": {
|
||||
"open": "Open {{name}} list",
|
||||
@@ -626,6 +628,14 @@
|
||||
"pin": {
|
||||
"label": "Pin",
|
||||
"ariaLabel": "Pin {{name}}"
|
||||
},
|
||||
"promote": {
|
||||
"label": "Make co-host",
|
||||
"ariaLabel": "Make {{name}} a co-host"
|
||||
},
|
||||
"demote": {
|
||||
"label": "Remove co-host role",
|
||||
"ariaLabel": "Remove {{name}}'s co-host role"
|
||||
}
|
||||
},
|
||||
"pinAnnouncements": {
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
"unknown": "Participant inconnu",
|
||||
"contributors": "Contributeurs",
|
||||
"host": "Organisateur de la réunion",
|
||||
"cohost": "Co-organisateur",
|
||||
"member": "Membre",
|
||||
"collapsable": {
|
||||
"open": "Ouvrir la liste {{name}}",
|
||||
"close": "Fermer la liste {{name}}"
|
||||
@@ -626,6 +628,14 @@
|
||||
"pin": {
|
||||
"label": "Épingler",
|
||||
"ariaLabel": "Épingler {{name}}"
|
||||
},
|
||||
"promote": {
|
||||
"label": "Nommer co-organisateur",
|
||||
"ariaLabel": "Nommer {{name}} co-organisateur"
|
||||
},
|
||||
"demote": {
|
||||
"label": "Retirer le rôle de co-organisateur",
|
||||
"ariaLabel": "Retirer le rôle de co-organisateur à {{name}}"
|
||||
}
|
||||
},
|
||||
"pinAnnouncements": {
|
||||
|
||||
@@ -582,6 +582,8 @@
|
||||
"you": "U",
|
||||
"unknown": "Onbekende deelnemer",
|
||||
"host": "Host",
|
||||
"cohost": "Co-host",
|
||||
"member": "Lid",
|
||||
"contributors": "Deelnemers",
|
||||
"collapsable": {
|
||||
"open": "Open {{name}} lijst",
|
||||
@@ -626,6 +628,14 @@
|
||||
"pin": {
|
||||
"label": "Pinnen",
|
||||
"ariaLabel": "Maak {{name}} vast"
|
||||
},
|
||||
"promote": {
|
||||
"label": "Co-host maken",
|
||||
"ariaLabel": "{{name}} co-host maken"
|
||||
},
|
||||
"demote": {
|
||||
"label": "Co-hostrol verwijderen",
|
||||
"ariaLabel": "De co-hostrol van {{name}} verwijderen"
|
||||
}
|
||||
},
|
||||
"pinAnnouncements": {
|
||||
|
||||
Reference in New Issue
Block a user