mirror of
https://github.com/suitenumerique/meet.git
synced 2026-10-11 07:40:35 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7f9522943 |
@@ -177,18 +177,31 @@ export const MainNotificationToast = () => {
|
||||
changedAttributes: Record<string, string>,
|
||||
participant: Participant
|
||||
) => {
|
||||
if (!participant.isLocal || !('room_role' in changedAttributes)) return
|
||||
const newRole = changedAttributes['room_role']
|
||||
toastQueue.add(
|
||||
{
|
||||
participant,
|
||||
type: NotificationType.RoleChanged,
|
||||
newRole: newRole,
|
||||
},
|
||||
{
|
||||
timeout: NotificationDuration.ROLE_CHANGED,
|
||||
}
|
||||
)
|
||||
if (!participant.isLocal) return
|
||||
if ('room_role' in changedAttributes) {
|
||||
toastQueue.add(
|
||||
{
|
||||
participant,
|
||||
type: NotificationType.RoleChanged,
|
||||
newRole: changedAttributes['room_role'],
|
||||
},
|
||||
{
|
||||
timeout: NotificationDuration.ROLE_CHANGED,
|
||||
}
|
||||
)
|
||||
}
|
||||
if ('is_speaker' in changedAttributes) {
|
||||
toastQueue.add(
|
||||
{
|
||||
participant,
|
||||
type: NotificationType.SpeakerChanged,
|
||||
isSpeaker: changedAttributes['is_speaker'] === 'true',
|
||||
},
|
||||
{
|
||||
timeout: NotificationDuration.ROLE_CHANGED,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
room.on(RoomEvent.ParticipantAttributesChanged, handleAttributeChanged)
|
||||
|
||||
|
||||
@@ -22,4 +22,5 @@ export enum NotificationType {
|
||||
RecordingSaving = 'recordingSaving',
|
||||
PermissionsRemoved = 'permissionsRemoved',
|
||||
RoleChanged = 'roleChanged',
|
||||
SpeakerChanged = 'speakerChanged',
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ToastPermissionsRemoved } from './ToastPermissionsRemoved'
|
||||
import { ToastRecordingRequest } from './ToastRecordingRequest'
|
||||
import { ToastAutoMuteLargeRoom } from './ToastAutoMuteLargeRoom'
|
||||
import { ToastRoleChanged } from '@/features/notifications/components/ToastRoleChanged'
|
||||
import { ToastSpeakerChanged } from './ToastSpeakerChanged'
|
||||
|
||||
interface ToastRegionProps extends AriaToastRegionProps {
|
||||
state: ToastState<ToastData>
|
||||
@@ -78,6 +79,9 @@ const renderToast = (
|
||||
case NotificationType.RoleChanged:
|
||||
return <ToastRoleChanged key={toast.key} toast={toast} state={state} />
|
||||
|
||||
case NotificationType.SpeakerChanged:
|
||||
return <ToastSpeakerChanged key={toast.key} toast={toast} state={state} />
|
||||
|
||||
default:
|
||||
return <Toast key={toast.key} toast={toast} state={state} />
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useToast } from 'react-aria'
|
||||
import { useRef } from 'react'
|
||||
|
||||
import { type ToastProps } from './Toast'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyledToastContainer } from './StyledToastContainer'
|
||||
|
||||
export function ToastSpeakerChanged({ state, ...props }: Readonly<ToastProps>) {
|
||||
const { t } = useTranslation('notifications', { keyPrefix: 'speakerChanged' })
|
||||
const ref = useRef(null)
|
||||
const { toastProps, contentProps } = useToast(props, state, ref)
|
||||
|
||||
return (
|
||||
<StyledToastContainer {...toastProps} ref={ref}>
|
||||
<HStack
|
||||
justify="center"
|
||||
alignItems="center"
|
||||
{...contentProps}
|
||||
padding={14}
|
||||
gap={0}
|
||||
>
|
||||
{t(props.toast.content.isSpeaker ? 'granted' : 'removed')}
|
||||
</HStack>
|
||||
</StyledToastContainer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { Participant, Track } from 'livekit-client'
|
||||
import { fetchApi } from '@/api/fetchApi'
|
||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||
import { useDefaultPublishSources } from '@/features/rooms/livekit/hooks/useRoomPublishSources'
|
||||
import { reportError } from '@/features/analytics/telemetry'
|
||||
|
||||
export const SPEAKER_ATTRIBUTE = 'is_speaker'
|
||||
|
||||
export const getParticipantIsSpeaker = (participant: Participant): boolean =>
|
||||
participant.attributes?.[SPEAKER_ATTRIBUTE] === 'true'
|
||||
|
||||
export const useParticipantSpeaker = () => {
|
||||
const data = useRoomData()
|
||||
const defaultSources = useDefaultPublishSources()
|
||||
|
||||
const updateSpeaker = async (
|
||||
participant: Participant,
|
||||
isSpeaker: boolean,
|
||||
sources: Track.Source[]
|
||||
) => {
|
||||
if (!data?.id) {
|
||||
throw new Error('Room id is not available')
|
||||
}
|
||||
|
||||
const attributes: Record<string, string> = {
|
||||
[SPEAKER_ATTRIBUTE]: isSpeaker ? 'true' : '',
|
||||
}
|
||||
|
||||
try {
|
||||
return await fetchApi(`rooms/${data.id}/update-participant/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
participant_identity: participant.identity,
|
||||
attributes,
|
||||
permission: {
|
||||
can_subscribe: participant.permissions?.canSubscribe,
|
||||
can_publish_data: participant.permissions?.canPublishData,
|
||||
can_update_metadata: participant.permissions?.canUpdateMetadata,
|
||||
can_subscribe_metrics: participant.permissions?.canSubscribeMetrics,
|
||||
can_publish: sources.length > 0,
|
||||
can_publish_sources: sources,
|
||||
},
|
||||
}),
|
||||
})
|
||||
} catch (error) {
|
||||
reportError('permissions_api_failure', error, {
|
||||
context: `Failed to update speaker status of ${participant.identity}`,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const makeSpeaker = (participant: Participant) =>
|
||||
updateSpeaker(participant, true, defaultSources)
|
||||
|
||||
const removeSpeaker = (
|
||||
participant: Participant,
|
||||
roomSources: Track.Source[]
|
||||
) => updateSpeaker(participant, false, roomSources)
|
||||
|
||||
return { makeSpeaker, removeSpeaker }
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { type LocalParticipant, type Participant, Track } from 'livekit-client'
|
||||
import { isLocal } from '@/utils/livekit'
|
||||
import {
|
||||
useIsSpeaking,
|
||||
useParticipantAttribute,
|
||||
useTrackMutedIndicator,
|
||||
} from '@livekit/components-react'
|
||||
import Source = Track.Source
|
||||
@@ -86,6 +87,16 @@ type ParticipantListItemProps = {
|
||||
export const ParticipantRow = ({ participant }: ParticipantListItemProps) => {
|
||||
const { t } = useTranslation('rooms')
|
||||
const name = participant.name || participant.identity
|
||||
const isSpeaker =
|
||||
useParticipantAttribute('is_speaker', { participant }) === 'true'
|
||||
|
||||
const roleLabels = [
|
||||
getParticipantIsRoomOwner(participant) && t('participants.host'),
|
||||
getParticipantIsRoomAdmin(participant) && t('participants.cohost'),
|
||||
getParticipantIsRoomMember(participant) && t('participants.member'),
|
||||
isSpeaker && t('participants.speaker'),
|
||||
].filter(Boolean)
|
||||
|
||||
return (
|
||||
<HStack
|
||||
role="listitem"
|
||||
@@ -107,12 +118,7 @@ export const ParticipantRow = ({ participant }: ParticipantListItemProps) => {
|
||||
displayedName={name}
|
||||
isLocal={isLocal(participant)}
|
||||
/>
|
||||
<Text variant="xsNote">
|
||||
{getParticipantIsRoomOwner(participant) && t('participants.host')}
|
||||
{getParticipantIsRoomAdmin(participant) && t('participants.cohost')}
|
||||
{getParticipantIsRoomMember(participant) &&
|
||||
t('participants.member')}
|
||||
</Text>
|
||||
<Text variant="xsNote">{roleLabels.join(' · ')}</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<HStack flexShrink={0}>
|
||||
|
||||
@@ -3,8 +3,10 @@ import type { Participant } from 'livekit-client'
|
||||
import { PinMenuItem } from './items/PinMenuItem'
|
||||
import { RemoveMenuItem } from './items/RemoveMenuItem'
|
||||
import { PromoteMenuItem } from './items/PromoteMenuItem'
|
||||
import { SpeakerMenuItem } from './items/SpeakerMenuItem'
|
||||
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
|
||||
import { useParticipantAttributes } from '@livekit/components-react'
|
||||
import { useHasLockedSource } from '@/features/rooms/livekit/hooks/useRoomPublishSources'
|
||||
|
||||
export const ParticipantMenu = ({
|
||||
participant,
|
||||
@@ -18,8 +20,13 @@ export const ParticipantMenu = ({
|
||||
const isAdmin = attributes?.room_role === 'administrator'
|
||||
const isOwner = attributes?.room_role === 'owner'
|
||||
const isAuthenticated = attributes?.is_authenticated == 'true'
|
||||
const isSpeaker = attributes?.is_speaker === 'true'
|
||||
|
||||
const hasLockedSource = useHasLockedSource()
|
||||
|
||||
const canManage = !participant.isLocal && isLocalUserAdminOrOwner && !isOwner
|
||||
const canToggleSpeaker =
|
||||
canManage && !isAdmin && (isSpeaker || hasLockedSource)
|
||||
|
||||
return (
|
||||
<RACMenu
|
||||
@@ -41,6 +48,9 @@ export const ParticipantMenu = ({
|
||||
displayedName={participant.name}
|
||||
/>
|
||||
)}
|
||||
{canToggleSpeaker && (
|
||||
<SpeakerMenuItem participant={participant} isSpeaker={isSpeaker} />
|
||||
)}
|
||||
</RACMenu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { MenuItem } from 'react-aria-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Participant } from 'livekit-client'
|
||||
import { RiMicOffLine, RiUserVoiceLine } from '@remixicon/react'
|
||||
import { useParticipantSpeaker } from '@/features/participants/api/updateParticipantSpeaker'
|
||||
import { useRoomPublishSources } from '@/features/rooms/livekit/hooks/useRoomPublishSources'
|
||||
import { menuRecipe } from '@/primitives/menuRecipe'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
|
||||
type SpeakerMenuItemProps = {
|
||||
participant: Participant
|
||||
isSpeaker: boolean
|
||||
}
|
||||
|
||||
export const SpeakerMenuItem = React.memo(
|
||||
({ participant, isSpeaker }: SpeakerMenuItemProps) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'participantMenu' })
|
||||
const { makeSpeaker, removeSpeaker } = useParticipantSpeaker()
|
||||
const roomSources = useRoomPublishSources()
|
||||
|
||||
const label = isSpeaker ? 'removeSpeaker' : 'makeSpeaker'
|
||||
const Icon = isSpeaker ? RiMicOffLine : RiUserVoiceLine
|
||||
|
||||
const toggleSpeaker = useCallback(
|
||||
() =>
|
||||
isSpeaker
|
||||
? removeSpeaker(participant, roomSources)
|
||||
: makeSpeaker(participant),
|
||||
[isSpeaker, participant, roomSources, makeSpeaker, removeSpeaker]
|
||||
)
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
aria-label={t(`${label}.ariaLabel`, {
|
||||
name: participant.name || participant.identity,
|
||||
})}
|
||||
className={menuRecipe({ icon: true }).item}
|
||||
onAction={toggleSpeaker}
|
||||
>
|
||||
<HStack gap={0.25} minWidth={280}>
|
||||
<Icon size={20} aria-hidden />
|
||||
{t(`${label}.label`)}
|
||||
</HStack>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SpeakerMenuItem.displayName = 'SpeakerMenuItem'
|
||||
@@ -1,12 +1,19 @@
|
||||
import { RoomEvent, Track } from 'livekit-client'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { useCallback } from 'react'
|
||||
import { usePatchRoom } from '@/features/rooms/api/patchRoom'
|
||||
import { useRemoteParticipants } from '@livekit/components-react'
|
||||
import { useUpdateParticipantsPermissions } from '@/features/rooms/api/updateParticipantsPermissions'
|
||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||
import { isSubsetOf } from '@/features/rooms/utils/isSubsetOf'
|
||||
import { getParticipantIsRoomAdminOrOwner } from '@/features/rooms/utils/getParticipantIsRoomAdminOrOwner'
|
||||
import {
|
||||
getParticipantIsSpeaker,
|
||||
useParticipantSpeaker,
|
||||
} from '@/features/participants/api/updateParticipantSpeaker'
|
||||
import {
|
||||
useDefaultPublishSources,
|
||||
useRoomPublishSources,
|
||||
} from '@/features/rooms/livekit/hooks/useRoomPublishSources'
|
||||
import Source = Track.Source
|
||||
import {
|
||||
NotificationType,
|
||||
@@ -33,8 +40,9 @@ export const usePublishSourcesManager = () => {
|
||||
const { mutateAsync: patchRoom } = usePatchRoom()
|
||||
|
||||
const data = useRoomData()
|
||||
const { data: configData } = useConfig()
|
||||
const configuration = data?.configuration
|
||||
const currentSources = useRoomPublishSources()
|
||||
const defaultSources = useDefaultPublishSources()
|
||||
|
||||
const { notifyParticipants } = useNotifyParticipants()
|
||||
|
||||
@@ -42,25 +50,18 @@ export const usePublishSourcesManager = () => {
|
||||
const roomId = data?.slug
|
||||
|
||||
const { updateParticipantsPermissions } = useUpdateParticipantsPermissions()
|
||||
const { removeSpeaker } = useParticipantSpeaker()
|
||||
const remoteParticipants = useRemoteParticipants({
|
||||
updateOnlyOn: [RoomEvent.ParticipantAttributesChanged],
|
||||
})
|
||||
|
||||
const unprivilegedRemoteParticipants = remoteParticipants.filter(
|
||||
(participant) => !getParticipantIsRoomAdminOrOwner(participant)
|
||||
const restrictedRemoteParticipants = remoteParticipants.filter(
|
||||
(participant) =>
|
||||
!getParticipantIsRoomAdminOrOwner(participant) &&
|
||||
!getParticipantIsSpeaker(participant)
|
||||
)
|
||||
|
||||
const currentSources = useMemo(() => {
|
||||
const defaultSources = configData?.livekit?.default_sources ?? []
|
||||
|
||||
if (
|
||||
configuration?.can_publish_sources == undefined ||
|
||||
!Array.isArray(configuration?.can_publish_sources)
|
||||
) {
|
||||
return defaultSources
|
||||
}
|
||||
return configuration.can_publish_sources
|
||||
}, [configData, configuration?.can_publish_sources])
|
||||
const speakers = remoteParticipants.filter(getParticipantIsSpeaker)
|
||||
|
||||
const updateSource = useCallback(
|
||||
async (sources: Source[], enabled: boolean) => {
|
||||
@@ -84,10 +85,16 @@ export const usePublishSourcesManager = () => {
|
||||
})
|
||||
|
||||
await updateParticipantsPermissions(
|
||||
unprivilegedRemoteParticipants,
|
||||
restrictedRemoteParticipants,
|
||||
newSources
|
||||
)
|
||||
|
||||
if (isSubsetOf(defaultSources, newSources)) {
|
||||
await Promise.all(
|
||||
speakers.map((speaker) => removeSpeaker(speaker, newSources))
|
||||
)
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
/*
|
||||
* We can't rely solely on the ParticipantPermissionsChanged event here,
|
||||
@@ -96,7 +103,7 @@ export const usePublishSourcesManager = () => {
|
||||
* */
|
||||
await notifyParticipants({
|
||||
type: NotificationType.PermissionsRemoved,
|
||||
destinationIdentities: unprivilegedRemoteParticipants.map(
|
||||
destinationIdentities: restrictedRemoteParticipants.map(
|
||||
(p) => p.identity
|
||||
),
|
||||
additionalData: {
|
||||
@@ -119,9 +126,12 @@ export const usePublishSourcesManager = () => {
|
||||
notifyParticipants,
|
||||
configuration,
|
||||
currentSources,
|
||||
defaultSources,
|
||||
roomId,
|
||||
patchRoom,
|
||||
unprivilegedRemoteParticipants,
|
||||
restrictedRemoteParticipants,
|
||||
speakers,
|
||||
removeSpeaker,
|
||||
updateParticipantsPermissions,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Track } from 'livekit-client'
|
||||
import { useMemo } from 'react'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||
import { isSubsetOf } from '@/features/rooms/utils/isSubsetOf'
|
||||
import Source = Track.Source
|
||||
|
||||
const EMPTY_SOURCES: Source[] = []
|
||||
|
||||
export const useDefaultPublishSources = (): Source[] => {
|
||||
const { data: configData } = useConfig()
|
||||
return configData?.livekit?.default_sources ?? EMPTY_SOURCES
|
||||
}
|
||||
|
||||
export const useRoomPublishSources = (): Source[] => {
|
||||
const data = useRoomData()
|
||||
const defaultSources = useDefaultPublishSources()
|
||||
const roomSources = data?.configuration?.can_publish_sources
|
||||
|
||||
return useMemo(
|
||||
() => (Array.isArray(roomSources) ? roomSources : defaultSources),
|
||||
[defaultSources, roomSources]
|
||||
)
|
||||
}
|
||||
|
||||
export const useHasLockedSource = (): boolean => {
|
||||
const defaultSources = useDefaultPublishSources()
|
||||
const roomSources = useRoomPublishSources()
|
||||
return !isSubsetOf(defaultSources, roomSources)
|
||||
}
|
||||
@@ -68,5 +68,9 @@
|
||||
"member": "Mitglied"
|
||||
}
|
||||
},
|
||||
"speakerChanged": {
|
||||
"granted": "Du bist jetzt Sprecher. Du kannst dein Mikrofon, deine Kamera und die Bildschirmfreigabe nutzen.",
|
||||
"removed": "Du bist nicht mehr Sprecher."
|
||||
},
|
||||
"openMenu": "Menü öffnen"
|
||||
}
|
||||
|
||||
@@ -659,6 +659,7 @@
|
||||
"host": "Host",
|
||||
"cohost": "Co-host",
|
||||
"member": "Mitglied",
|
||||
"speaker": "Sprecher",
|
||||
"contributors": "Teilnehmende",
|
||||
"collapsable": {
|
||||
"open": "{{name}}-Liste öffnen",
|
||||
@@ -714,6 +715,14 @@
|
||||
"demote": {
|
||||
"label": "Co-Host-Rolle entfernen",
|
||||
"ariaLabel": "Die Co-Host-Rolle von {{name}} entfernen"
|
||||
},
|
||||
"makeSpeaker": {
|
||||
"label": "Zum Sprecher machen",
|
||||
"ariaLabel": "{{name}} zum Sprecher machen"
|
||||
},
|
||||
"removeSpeaker": {
|
||||
"label": "Sprecherrolle entfernen",
|
||||
"ariaLabel": "Sprecherrolle von {{name}} entfernen"
|
||||
}
|
||||
},
|
||||
"pinAnnouncements": {
|
||||
|
||||
@@ -68,5 +68,9 @@
|
||||
"member": "Member"
|
||||
}
|
||||
},
|
||||
"speakerChanged": {
|
||||
"granted": "You are now a speaker. You can use your microphone, camera and screen sharing.",
|
||||
"removed": "You are no longer a speaker."
|
||||
},
|
||||
"openMenu": "Open menu"
|
||||
}
|
||||
|
||||
@@ -659,6 +659,7 @@
|
||||
"host": "Host",
|
||||
"cohost": "Co-host",
|
||||
"member": "Member",
|
||||
"speaker": "Speaker",
|
||||
"contributors": "Contributors",
|
||||
"collapsable": {
|
||||
"open": "Open {{name}} list",
|
||||
@@ -714,6 +715,14 @@
|
||||
"demote": {
|
||||
"label": "Remove co-host role",
|
||||
"ariaLabel": "Remove {{name}}'s co-host role"
|
||||
},
|
||||
"makeSpeaker": {
|
||||
"label": "Make speaker",
|
||||
"ariaLabel": "Make {{name}} a speaker"
|
||||
},
|
||||
"removeSpeaker": {
|
||||
"label": "Remove speaker",
|
||||
"ariaLabel": "Remove {{name}} as speaker"
|
||||
}
|
||||
},
|
||||
"pinAnnouncements": {
|
||||
|
||||
@@ -68,5 +68,9 @@
|
||||
"member": "Miembro"
|
||||
}
|
||||
},
|
||||
"speakerChanged": {
|
||||
"granted": "Ahora eres ponente. Puedes usar tu micrófono, tu cámara y compartir pantalla.",
|
||||
"removed": "Ya no eres ponente."
|
||||
},
|
||||
"openMenu": "Abrir el menú"
|
||||
}
|
||||
|
||||
@@ -659,6 +659,7 @@
|
||||
"host": "Organizador de la reunión",
|
||||
"cohost": "Coorganizador",
|
||||
"member": "Miembro",
|
||||
"speaker": "Ponente",
|
||||
"collapsable": {
|
||||
"open": "Abrir la lista {{name}}",
|
||||
"close": "Cerrar la lista {{name}}"
|
||||
@@ -713,6 +714,14 @@
|
||||
"demote": {
|
||||
"label": "Retirar el rol de coorganizador",
|
||||
"ariaLabel": "Retirar el rol de coorganizador a {{name}}"
|
||||
},
|
||||
"makeSpeaker": {
|
||||
"label": "Nombrar ponente",
|
||||
"ariaLabel": "Nombrar ponente a {{name}}"
|
||||
},
|
||||
"removeSpeaker": {
|
||||
"label": "Quitar rol de ponente",
|
||||
"ariaLabel": "Quitar el rol de ponente a {{name}}"
|
||||
}
|
||||
},
|
||||
"pinAnnouncements": {
|
||||
|
||||
@@ -68,5 +68,9 @@
|
||||
"member": "Membre"
|
||||
}
|
||||
},
|
||||
"speakerChanged": {
|
||||
"granted": "Vous êtes maintenant intervenant. Vous pouvez utiliser votre micro, votre caméra et partager votre écran.",
|
||||
"removed": "Vous n'êtes plus intervenant."
|
||||
},
|
||||
"openMenu": "Ouvrir le menu"
|
||||
}
|
||||
|
||||
@@ -660,6 +660,7 @@
|
||||
"host": "Organisateur de la réunion",
|
||||
"cohost": "Co-organisateur",
|
||||
"member": "Membre",
|
||||
"speaker": "Intervenant",
|
||||
"collapsable": {
|
||||
"open": "Ouvrir la liste {{name}}",
|
||||
"close": "Fermer la liste {{name}}"
|
||||
@@ -714,6 +715,14 @@
|
||||
"demote": {
|
||||
"label": "Retirer le rôle de co-organisateur",
|
||||
"ariaLabel": "Retirer le rôle de co-organisateur à {{name}}"
|
||||
},
|
||||
"makeSpeaker": {
|
||||
"label": "Désigner comme intervenant",
|
||||
"ariaLabel": "Désigner {{name}} comme intervenant"
|
||||
},
|
||||
"removeSpeaker": {
|
||||
"label": "Retirer le rôle d'intervenant",
|
||||
"ariaLabel": "Retirer le rôle d'intervenant de {{name}}"
|
||||
}
|
||||
},
|
||||
"pinAnnouncements": {
|
||||
|
||||
@@ -68,5 +68,9 @@
|
||||
"member": "Lid"
|
||||
}
|
||||
},
|
||||
"speakerChanged": {
|
||||
"granted": "Je bent nu spreker. Je kunt je microfoon, camera en schermdeling gebruiken.",
|
||||
"removed": "Je bent geen spreker meer."
|
||||
},
|
||||
"openMenu": "Menu openen"
|
||||
}
|
||||
|
||||
@@ -659,6 +659,7 @@
|
||||
"host": "Host",
|
||||
"cohost": "Co-host",
|
||||
"member": "Lid",
|
||||
"speaker": "Spreker",
|
||||
"contributors": "Deelnemers",
|
||||
"collapsable": {
|
||||
"open": "Open {{name}} lijst",
|
||||
@@ -714,6 +715,14 @@
|
||||
"demote": {
|
||||
"label": "Co-hostrol verwijderen",
|
||||
"ariaLabel": "De co-hostrol van {{name}} verwijderen"
|
||||
},
|
||||
"makeSpeaker": {
|
||||
"label": "Spreker maken",
|
||||
"ariaLabel": "{{name}} spreker maken"
|
||||
},
|
||||
"removeSpeaker": {
|
||||
"label": "Sprekersrol verwijderen",
|
||||
"ariaLabel": "Sprekersrol van {{name}} verwijderen"
|
||||
}
|
||||
},
|
||||
"pinAnnouncements": {
|
||||
|
||||
Reference in New Issue
Block a user