diff --git a/src/frontend/src/features/notifications/MainNotificationToast.tsx b/src/frontend/src/features/notifications/MainNotificationToast.tsx index 0583278b..67724148 100644 --- a/src/frontend/src/features/notifications/MainNotificationToast.tsx +++ b/src/frontend/src/features/notifications/MainNotificationToast.tsx @@ -104,6 +104,19 @@ export const MainNotificationToast = () => { { timeout: NotificationDuration.ALERT } ) break + case NotificationType.PermissionsRemoved: { + const removedSources = notification?.data?.removedSources + if (!removedSources?.length) break + toastQueue.add( + { + participant, + type: notification.type, + removedSources: removedSources, + }, + { timeout: NotificationDuration.ALERT } + ) + break + } default: return } diff --git a/src/frontend/src/features/notifications/NotificationPayload.ts b/src/frontend/src/features/notifications/NotificationPayload.ts index 3ca55989..5c5a3a29 100644 --- a/src/frontend/src/features/notifications/NotificationPayload.ts +++ b/src/frontend/src/features/notifications/NotificationPayload.ts @@ -4,5 +4,6 @@ export interface NotificationPayload { type: NotificationType data?: { emoji?: string + removedSources?: string[] } } diff --git a/src/frontend/src/features/notifications/NotificationType.ts b/src/frontend/src/features/notifications/NotificationType.ts index 827a2323..58ea6622 100644 --- a/src/frontend/src/features/notifications/NotificationType.ts +++ b/src/frontend/src/features/notifications/NotificationType.ts @@ -13,4 +13,5 @@ export enum NotificationType { ScreenRecordingStopped = 'screenRecordingStopped', ScreenRecordingLimitReached = 'screenRecordingLimitReached', RecordingSaving = 'recordingSaving', + PermissionsRemoved = 'permissionsRemoved', } diff --git a/src/frontend/src/features/notifications/components/ToastPermissionsRemoved.tsx b/src/frontend/src/features/notifications/components/ToastPermissionsRemoved.tsx new file mode 100644 index 00000000..ac5002d3 --- /dev/null +++ b/src/frontend/src/features/notifications/components/ToastPermissionsRemoved.tsx @@ -0,0 +1,49 @@ +import { useToast } from '@react-aria/toast' +import { useMemo, useRef } from 'react' + +import { StyledToastContainer, ToastProps } from './Toast' +import { HStack } from '@/styled-system/jsx' +import { useTranslation } from 'react-i18next' + +export function ToastPermissionsRemoved({ state, ...props }: ToastProps) { + const { t } = useTranslation('notifications', { + keyPrefix: 'permissionsRemoved', + }) + const ref = useRef(null) + const { toastProps, contentProps } = useToast(props, state, ref) + const participant = props.toast.content.participant + + const key = useMemo(() => { + const sources = props.toast.content.removedSources + + if (!Array.isArray(sources) || sources.length === 0) { + return undefined + } + + if (sources.length === 1) { + return sources[0] + } + + if (sources.length === 2 && sources.includes('screen_share')) { + return 'screen_share' + } + + return undefined + }, [props.toast.content.removedSources]) + + if (!participant || !key) return null + + return ( + + + {t(key)} + + + ) +} diff --git a/src/frontend/src/features/notifications/components/ToastRegion.tsx b/src/frontend/src/features/notifications/components/ToastRegion.tsx index 05066c9d..24c724a0 100644 --- a/src/frontend/src/features/notifications/components/ToastRegion.tsx +++ b/src/frontend/src/features/notifications/components/ToastRegion.tsx @@ -11,6 +11,7 @@ import { ToastMessageReceived } from './ToastMessageReceived' import { ToastLowerHand } from './ToastLowerHand' import { ToastAnyRecording } from './ToastAnyRecording' import { ToastRecordingSaving } from './ToastRecordingSaving' +import { ToastPermissionsRemoved } from './ToastPermissionsRemoved' interface ToastRegionProps extends AriaToastRegionProps { state: ToastState @@ -30,6 +31,11 @@ const renderToast = ( case NotificationType.ParticipantMuted: return + case NotificationType.PermissionsRemoved: + return ( + + ) + case NotificationType.MessageReceived: return ( diff --git a/src/frontend/src/features/rooms/livekit/hooks/usePublishSourcesManager.ts b/src/frontend/src/features/rooms/livekit/hooks/usePublishSourcesManager.ts index 71a1bfd1..3ce92da8 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/usePublishSourcesManager.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/usePublishSourcesManager.ts @@ -10,6 +10,10 @@ import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData' import { isSubsetOf } from '@/features/rooms/utils/isSubsetOf' import { getParticipantIsRoomAdmin } from '@/features/rooms/utils/getParticipantIsRoomAdmin' import Source = Track.Source +import { + NotificationType, + useNotifyParticipants, +} from '@/features/notifications' export const updatePublishSources = ( currentSources: Source[], @@ -33,6 +37,8 @@ export const usePublishSourcesManager = () => { const { data: configData } = useConfig() const configuration = data?.configuration + const { notifyParticipants } = useNotifyParticipants() + const defaultSources = configData?.livekit?.default_sources?.map((source) => { return source as Source }) @@ -87,6 +93,25 @@ export const usePublishSourcesManager = () => { newSources ) + if (!enabled) { + /* + * We can't rely solely on the ParticipantPermissionsChanged event here, + * because for local participants it is emitted twice (once from Participant, once from LocalParticipant). + * livekit/client-sdk-js/issues/1637 + * */ + await notifyParticipants({ + type: NotificationType.PermissionsRemoved, + destinationIdentities: unprivilegedRemoteParticipants.map( + (p) => p.identity + ), + additionalData: { + data: { + removedSources: sources, + }, + }, + }) + } + return { configuration: newConfiguration } } catch (error) { console.error(`Failed to update ${sources}:`, error) @@ -94,6 +119,7 @@ export const usePublishSourcesManager = () => { } }, [ + notifyParticipants, configuration, currentSources, roomId, diff --git a/src/frontend/src/locales/de/notifications.json b/src/frontend/src/locales/de/notifications.json index f1060ec2..99df3c1b 100644 --- a/src/frontend/src/locales/de/notifications.json +++ b/src/frontend/src/locales/de/notifications.json @@ -16,6 +16,11 @@ "reaction": { "description": "{{name}} hat mit {{emoji}} reagiert" }, + "permissionsRemoved": { + "camera": "Der Organisator hat das Video für alle Teilnehmer deaktiviert.", + "microphone": "Der Organisator hat das Mikrofon für alle Teilnehmer deaktiviert.", + "screen_share": "Der Organisator hat die Bildschirmfreigabe für alle Teilnehmer deaktiviert." + }, "waitingParticipants": { "one": "Eine Person möchte diesem Anruf beitreten.", "several": "Mehrere Personen möchten diesem Anruf beitreten.", diff --git a/src/frontend/src/locales/en/notifications.json b/src/frontend/src/locales/en/notifications.json index 16908fd2..c9de8dfc 100644 --- a/src/frontend/src/locales/en/notifications.json +++ b/src/frontend/src/locales/en/notifications.json @@ -16,6 +16,11 @@ "reaction": { "description": "{{name}} reacted with {{emoji}}" }, + "permissionsRemoved": { + "camera": "The host has disabled video for all participants.", + "microphone": "The host has disabled the microphone for all participants.", + "screen_share": "The host has disabled screen sharing for all participants." + }, "waitingParticipants": { "one": "One person wants to join this call.", "several": "Several people want to join this call.", diff --git a/src/frontend/src/locales/fr/notifications.json b/src/frontend/src/locales/fr/notifications.json index 8f292077..bfec3048 100644 --- a/src/frontend/src/locales/fr/notifications.json +++ b/src/frontend/src/locales/fr/notifications.json @@ -16,6 +16,11 @@ "reaction": { "description": "{{name}} a reagi avec {{emoji}}" }, + "permissionsRemoved": { + "camera": "L'organisateur a désactivé la vidéo de tous les participants.", + "microphone": "L'organisateur a désactivé le micro de tous les participants.", + "screen_share": "L'organisateur a désactivé le partage d'écran de tous les participants." + }, "waitingParticipants": { "one": "Une personne souhaite participer à cet appel.", "several": "Plusieurs personnes souhaitent participer à cet appel.", diff --git a/src/frontend/src/locales/nl/notifications.json b/src/frontend/src/locales/nl/notifications.json index 4b1cd073..76c27469 100644 --- a/src/frontend/src/locales/nl/notifications.json +++ b/src/frontend/src/locales/nl/notifications.json @@ -16,6 +16,11 @@ "reaction": { "description": "{{name}} reageerde met {{emoji}}" }, + "permissionsRemoved": { + "camera": "De organisator heeft de video voor alle deelnemers uitgeschakeld.", + "microphone": "De organisator heeft de microfoon voor alle deelnemers uitgeschakeld.", + "screen_share": "De organisator heeft het schermdelen voor alle deelnemers uitgeschakeld." + }, "waitingParticipants": { "one": "Eén persoon wil deelnemen aan dit gesprek.", "several": "Meerdere mensen willen deelnemen aan dit gesprek.",