(frontend) display host status to meeting participants

Add host identification display for participants using boolean flag from
LiveKit token attributes. Currently passes simple boolean but will be
refactored to distinguish owner/admin/member roles and host/co-host
with different privileges.

Security note: attributes are not fully secure as participants can
update their own metadata, potentially faking admin status. However,
consequences are limited to user confusion without destructive
capabilities. Metadata updates currently needed for name changes and
hand raising functionality.

Plan to remove canUpdateOwnMetadata permission to strengthen security
while preserving essential user interaction capabilities.
This commit is contained in:
lebaudantoine
2025-08-27 15:25:18 +02:00
committed by aleb_the_flash
parent 1268346405
commit 4793f2fa8f
6 changed files with 43 additions and 21 deletions

View File

@@ -1,10 +1,11 @@
import { css } from '@/styled-system/css'
import { HStack } from '@/styled-system/jsx'
import { HStack, VStack } from '@/styled-system/jsx'
import { Text } from '@/primitives/Text'
import { useTranslation } from 'react-i18next'
import { Avatar } from '@/components/Avatar'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { getParticipantIsRoomAdmin } from '@/features/rooms/utils/getParticipantIsRoomAdmin'
import { Participant, Track } from 'livekit-client'
import { isLocal } from '@/utils/livekit'
import {
@@ -102,36 +103,41 @@ export const ParticipantListItem = ({
>
<HStack>
<Avatar name={name} bgColor={getParticipantColor(participant)} />
<Text
variant={'sm'}
className={css({
userSelect: 'none',
cursor: 'default',
display: 'flex',
})}
>
<span
<VStack gap={0} alignItems="start">
<Text
sm
className={css({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '120px',
display: 'block',
userSelect: 'none',
cursor: 'default',
display: 'flex',
})}
>
{name}
</span>
{isLocal(participant) && (
<span
className={css({
marginLeft: '.25rem',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '120px',
display: 'block',
})}
>
({t('participants.you')})
{name}
</span>
{isLocal(participant) && (
<span
className={css({
marginLeft: '.25rem',
whiteSpace: 'nowrap',
})}
>
({t('participants.you')})
</span>
)}
</Text>
{getParticipantIsRoomAdmin(participant) && (
<Text variant="xsNote">{t('participants.host')}</Text>
)}
</Text>
</VStack>
</HStack>
<HStack>
<MicIndicator participant={participant} />

View File

@@ -0,0 +1,12 @@
import { Participant } from 'livekit-client'
export const getParticipantIsRoomAdmin = (
participant: Participant
): boolean => {
const attributes = participant.attributes
if (!attributes) {
return false
}
return attributes?.room_admin === 'true'
}

View File

@@ -426,6 +426,7 @@
"participants": {
"subheading": "Im Raum",
"you": "Du",
"host": "Host",
"contributors": "Mitwirkende",
"collapsable": {
"open": "{{name}}-Liste öffnen",

View File

@@ -426,6 +426,7 @@
"participants": {
"subheading": "In room",
"you": "You",
"host": "Host",
"contributors": "Contributors",
"collapsable": {
"open": "Open {{name}} list",

View File

@@ -427,6 +427,7 @@
"subheading": "Dans la réunion",
"you": "Vous",
"contributors": "Contributeurs",
"host": "Organisateur de la réunion",
"collapsable": {
"open": "Ouvrir la liste {{name}}",
"close": "Fermer la liste {{name}}"

View File

@@ -426,6 +426,7 @@
"participants": {
"subheading": "In de ruimte",
"you": "U",
"host": "Host",
"contributors": "Deelnemers",
"collapsable": {
"open": "Open {{name}} lijst",