From 040df0e15a7063d9229dd9a257d0887d0f39c1d9 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Mon, 15 Jun 2026 15:42:47 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=B8(frontend)=20mute=20participants=20?= =?UTF-8?q?by=20default=20when=20joining=20a=20large=20meeting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When joining a meeting that already has many participants, new participants are now muted by default. This is an empirical change, not directly requested by users but informed by user experience: a lot of people joining large meetings arrive with their microphone and/or camera open, which can be painful for the host, who otherwise has to mute everyone or ask everyone to mute. Many of these participants are inattentive but still noisy. If you are joining a room that is already at a certain size, you are probably not the one expected to speak; presenters typically join among the very first participants. --- CHANGELOG.md | 1 + src/backend/meet/settings.py | 5 +- src/frontend/src/api/useConfig.ts | 1 + .../notifications/NotificationType.ts | 1 + .../components/ToastAutoMuteLargeRoom.tsx | 54 +++++++++++++++++++ .../notifications/components/ToastRegion.tsx | 6 +++ .../src/features/notifications/utils.ts | 9 ++++ .../features/rooms/components/Conference.tsx | 22 +++++++- .../settings/components/tabs/GeneralTab.tsx | 13 +++++ .../src/locales/de/notifications.json | 4 ++ src/frontend/src/locales/de/settings.json | 4 ++ .../src/locales/en/notifications.json | 4 ++ src/frontend/src/locales/en/settings.json | 4 ++ .../src/locales/fr/notifications.json | 4 ++ src/frontend/src/locales/fr/settings.json | 4 ++ .../src/locales/nl/notifications.json | 4 ++ src/frontend/src/locales/nl/settings.json | 4 ++ src/frontend/src/stores/userPreferences.ts | 2 + 18 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 src/frontend/src/features/notifications/components/ToastAutoMuteLargeRoom.tsx 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 {