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.",