diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dd5a500..628197b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to - ✨(frontend) enhance noise reduction with BBBA audio processing pipeline - 🚸(frontend) mute join notification sound in larger rooms +- 🚸(frontend) mute participants by default when joining a large meeting ## [1.20.0] - 2026-06-12 diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index 9fa2c1a0..05ce3edb 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -413,7 +413,10 @@ class Base(Configuration): ), "max_participants_for_sound": values.PositiveIntegerValue( 5, environ_name="FRONTEND_MAX_PARTICIPANTS_FOR_SOUND", environ_prefix=None - ) + ), + "auto_mute_on_join_threshold": values.PositiveIntegerValue( + 50, environ_name="FRONTEND_AUTO_MUTE_ON_JOIN_THRESHOLD", environ_prefix=None + ), } # Mail diff --git a/src/frontend/src/api/useConfig.ts b/src/frontend/src/api/useConfig.ts index 1265b59e..943f2c38 100644 --- a/src/frontend/src/api/useConfig.ts +++ b/src/frontend/src/api/useConfig.ts @@ -56,6 +56,7 @@ export interface ApiConfig { } transcription_destination?: string max_participants_for_sound: number + auto_mute_on_join_threshold: number } const fetchConfig = (): Promise => { diff --git a/src/frontend/src/features/notifications/NotificationType.ts b/src/frontend/src/features/notifications/NotificationType.ts index 7eadc929..1475e9c2 100644 --- a/src/frontend/src/features/notifications/NotificationType.ts +++ b/src/frontend/src/features/notifications/NotificationType.ts @@ -1,4 +1,5 @@ export enum NotificationType { + AutoMuteLargeRoom = 'autoMuteLargeRoom', ParticipantJoined = 'participantJoined', HandRaised = 'handRaised', ParticipantMuted = 'participantMuted', diff --git a/src/frontend/src/features/notifications/components/ToastAutoMuteLargeRoom.tsx b/src/frontend/src/features/notifications/components/ToastAutoMuteLargeRoom.tsx new file mode 100644 index 00000000..e72e8f9e --- /dev/null +++ b/src/frontend/src/features/notifications/components/ToastAutoMuteLargeRoom.tsx @@ -0,0 +1,54 @@ +import { useToast } from '@react-aria/toast' +import { useRef } from 'react' + +import { type ToastProps } from './Toast' +import { VStack } from '@/styled-system/jsx' +import { useTranslation } from 'react-i18next' +import { Button } from '@/primitives' +import { css } from '@/styled-system/css' +import { StyledToastContainer } from './StyledToastContainer' +import { useRoomContext } from '@livekit/components-react' + +export function ToastAutoMuteLargeRoom({ + state, + ...props +}: Readonly) { + const room = useRoomContext() + const { t } = useTranslation('notifications', { + keyPrefix: 'autoMuteLargeRoom', + }) + const ref = useRef(null) + const { toastProps, contentProps } = useToast(props, state, ref) + const toast = props.toast + + const handleDismiss = async () => { + room.localParticipant + .setMicrophoneEnabled(true) + .finally(() => state.close(toast.key)) + } + + return ( + + +

{t('auto')}

+ +
+
+ ) +} diff --git a/src/frontend/src/features/notifications/components/ToastRegion.tsx b/src/frontend/src/features/notifications/components/ToastRegion.tsx index 64a3a321..45fb55fa 100644 --- a/src/frontend/src/features/notifications/components/ToastRegion.tsx +++ b/src/frontend/src/features/notifications/components/ToastRegion.tsx @@ -13,6 +13,7 @@ import { ToastAnyRecording } from './ToastAnyRecording' import { ToastRecordingSaving } from './ToastRecordingSaving' import { ToastPermissionsRemoved } from './ToastPermissionsRemoved' import { ToastRecordingRequest } from './ToastRecordingRequest' +import { ToastAutoMuteLargeRoom } from './ToastAutoMuteLargeRoom' interface ToastRegionProps extends AriaToastRegionProps { state: ToastState @@ -45,6 +46,11 @@ const renderToast = ( case NotificationType.LowerHand: return + case NotificationType.AutoMuteLargeRoom: + return ( + + ) + case NotificationType.TranscriptionStarted: case NotificationType.TranscriptionStopped: case NotificationType.TranscriptionLimitReached: diff --git a/src/frontend/src/features/notifications/utils.ts b/src/frontend/src/features/notifications/utils.ts index 899ca5e0..a43dbe34 100644 --- a/src/frontend/src/features/notifications/utils.ts +++ b/src/frontend/src/features/notifications/utils.ts @@ -5,6 +5,15 @@ import type { Participant } from 'livekit-client' import type { NotificationPayload } from './NotificationPayload' import type { RecordingMode } from '@/features/recording' +export const notifyAutoMutedOnJoin = () => { + toastQueue.add( + { + type: NotificationType.AutoMuteLargeRoom, + }, + { timeout: NotificationDuration.ALERT } + ) +} + export const showLowerHandToast = ( participant: Participant, onClose: () => void diff --git a/src/frontend/src/features/rooms/components/Conference.tsx b/src/frontend/src/features/rooms/components/Conference.tsx index 35e37277..27ee3c10 100644 --- a/src/frontend/src/features/rooms/components/Conference.tsx +++ b/src/frontend/src/features/rooms/components/Conference.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { @@ -33,6 +33,9 @@ import { useIsMobile } from '@/utils/useIsMobile' import { navigateTo } from '@/navigation/navigateTo' import { connectionObserverStore } from '@/stores/connectionObserver' import { PictureInPictureConference } from '@/features/pip/components/PictureInPictureConference' +import { notifyAutoMutedOnJoin } from '@/features/notifications/utils' +import { useSnapshot } from 'valtio' +import { userPreferencesStore } from '@/stores/userPreferences' export const Conference = ({ roomId, @@ -57,6 +60,8 @@ export const Conference = ({ const [isConnectionWarmedUp, setIsConnectionWarmedUp] = useState(false) + const userPreferencesSnap = useSnapshot(userPreferencesStore) + const { mutateAsync: createRoom, status: createStatus, @@ -173,6 +178,8 @@ export const Conference = ({ const isMobile = useIsMobile() + const hasAutoMutedRef = useRef(false) + /* * Ensure stable WebSocket connection URL. This is critical for legacy browser compatibility * (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs in WebSocket() constructor @@ -228,6 +235,19 @@ export const Conference = ({ onError={(e) => { posthog.captureException(e) }} + onConnected={async () => { + if (!apiConfig) return + if ( + userPreferencesSnap.is_auto_mute_large_room_enabled && + !hasAutoMutedRef.current && + userConfig.audioEnabled && + room.numParticipants > apiConfig.auto_mute_on_join_threshold + ) { + hasAutoMutedRef.current = true + await room.localParticipant.setMicrophoneEnabled(false) + notifyAutoMutedOnJoin() + } + }} onDisconnected={(e) => { const metadata = { room_id: roomId, diff --git a/src/frontend/src/features/settings/components/tabs/GeneralTab.tsx b/src/frontend/src/features/settings/components/tabs/GeneralTab.tsx index e07d357c..d86bfdcc 100644 --- a/src/frontend/src/features/settings/components/tabs/GeneralTab.tsx +++ b/src/frontend/src/features/settings/components/tabs/GeneralTab.tsx @@ -39,6 +39,19 @@ export const GeneralTab = ({ id }: GeneralTabProps) => { fullWidth: true, }} /> + + (userPreferencesStore.is_auto_mute_large_room_enabled = value) + } + wrapperProps={{ + noMargin: true, + fullWidth: true, + }} + /> ) } diff --git a/src/frontend/src/locales/de/notifications.json b/src/frontend/src/locales/de/notifications.json index 356ed2a6..67f495b6 100644 --- a/src/frontend/src/locales/de/notifications.json +++ b/src/frontend/src/locales/de/notifications.json @@ -14,6 +14,10 @@ "auto": "Du hast begonnen zu sprechen – deine Hand wird daher automatisch gesenkt.", "dismiss": "Hand oben lassen" }, + "autoMuteLargeRoom": { + "auto": "Sie haben an einer großen Besprechung teilgenommen. Ihr Mikrofon wurde automatisch stummgeschaltet.", + "dismiss": "Stummschaltung aufheben" + }, "reaction": { "description": "{{name}} hat mit {{emoji}} reagiert" }, diff --git a/src/frontend/src/locales/de/settings.json b/src/frontend/src/locales/de/settings.json index 8c13a096..9dfaffd8 100644 --- a/src/frontend/src/locales/de/settings.json +++ b/src/frontend/src/locales/de/settings.json @@ -12,6 +12,10 @@ "idleDisconnectModal": { "label": "Meetings ohne Teilnehmende verlassen", "description": "Meetings nach einigen Minuten automatisch verlassen, wenn keine andere Person beitritt" + }, + "autoMuteLargeRoom": { + "label": "Mikrofon automatisch stummschalten", + "description": "Dein Mikrofon wird automatisch stummgeschaltet, wenn du an einer großen Besprechung teilnimmst." } }, "audio": { diff --git a/src/frontend/src/locales/en/notifications.json b/src/frontend/src/locales/en/notifications.json index 665bb94d..77d724bd 100644 --- a/src/frontend/src/locales/en/notifications.json +++ b/src/frontend/src/locales/en/notifications.json @@ -14,6 +14,10 @@ "auto": "It seems you have started speaking, so your hand will be lowered.", "dismiss": "Keep hand raised" }, + "autoMuteLargeRoom": { + "auto": "You have joined a large meeting. Your microphone has been automatically muted.", + "dismiss": "Unmute" + }, "reaction": { "description": "{{name}} reacted with {{emoji}}" }, diff --git a/src/frontend/src/locales/en/settings.json b/src/frontend/src/locales/en/settings.json index ff79348d..7f79fcd7 100644 --- a/src/frontend/src/locales/en/settings.json +++ b/src/frontend/src/locales/en/settings.json @@ -12,6 +12,10 @@ "idleDisconnectModal": { "label": "Leave calls with no participants", "description": "Automatically leaves a call after a few minutes if no other participant joins" + }, + "autoMuteLargeRoom": { + "label": "Automatically mute my microphone", + "description": "Automatically mute your microphone when joining a large meeting." } }, "audio": { diff --git a/src/frontend/src/locales/fr/notifications.json b/src/frontend/src/locales/fr/notifications.json index 28a65e5f..0a84d453 100644 --- a/src/frontend/src/locales/fr/notifications.json +++ b/src/frontend/src/locales/fr/notifications.json @@ -14,6 +14,10 @@ "auto": "Il semblerait que vous ayez pris la parole, donc la main va être baissée.", "dismiss": "Laisser la main levée" }, + "autoMuteLargeRoom": { + "auto": "Vous venez de rejoindre une grande réunion. Votre micro a été automatiquement coupé.", + "dismiss": "Rétablir le micro" + }, "reaction": { "description": "{{name}} a reagi avec {{emoji}}" }, diff --git a/src/frontend/src/locales/fr/settings.json b/src/frontend/src/locales/fr/settings.json index 1e63c6bb..072c3ff4 100644 --- a/src/frontend/src/locales/fr/settings.json +++ b/src/frontend/src/locales/fr/settings.json @@ -12,6 +12,10 @@ "idleDisconnectModal": { "label": "Quitter les appels sans autre participant", "description": "Vous fait quitter un appel au bout de quelques minutes si aucun autre participant ne vous rejoint" + }, + "autoMuteLargeRoom": { + "label": "Couper automatiquement mon micro", + "description": "Coupe automatiquement votre micro lorsque vous rejoignez une réunion de grande taille." } }, "audio": { diff --git a/src/frontend/src/locales/nl/notifications.json b/src/frontend/src/locales/nl/notifications.json index 74778286..a90cd56f 100644 --- a/src/frontend/src/locales/nl/notifications.json +++ b/src/frontend/src/locales/nl/notifications.json @@ -14,6 +14,10 @@ "auto": "Het lijkt erop dat u bent begonnen te spreken, dus we laten uw hand zakken.", "dismiss": "Houdt uw hand opgestoken" }, + "autoMuteLargeRoom": { + "auto": "U bent zojuist lid geworden van een grote vergadering. Uw microfoon is automatisch gedempt.", + "dismiss": "Microfoon inschakelen" + }, "reaction": { "description": "{{name}} reageerde met {{emoji}}" }, diff --git a/src/frontend/src/locales/nl/settings.json b/src/frontend/src/locales/nl/settings.json index b7c023db..6d4a71d0 100644 --- a/src/frontend/src/locales/nl/settings.json +++ b/src/frontend/src/locales/nl/settings.json @@ -12,6 +12,10 @@ "idleDisconnectModal": { "label": "Verlaat oproepen zonder deelnemers", "description": "Verlaat automatisch een oproep na een paar minuten als geen andere deelnemer meedoet" + }, + "autoMuteLargeRoom": { + "label": "Mijn microfoon automatisch dempen", + "description": "Uw microfoon wordt automatisch gedempt wanneer u deelneemt aan een grote vergadering." } }, "audio": { diff --git a/src/frontend/src/stores/userPreferences.ts b/src/frontend/src/stores/userPreferences.ts index ca520790..0d19e1fd 100644 --- a/src/frontend/src/stores/userPreferences.ts +++ b/src/frontend/src/stores/userPreferences.ts @@ -3,10 +3,12 @@ import { STORAGE_KEYS } from '@/utils/storageKeys' type State = { is_idle_disconnect_modal_enabled: boolean + is_auto_mute_large_room_enabled: boolean } const DEFAULT_STATE = { is_idle_disconnect_modal_enabled: true, + is_auto_mute_large_room_enabled: true, } function getUserPreferencesState(): State {