diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59cc7172..36f336ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,7 @@ and this project adheres to
- 🐛(frontend) treat client-initiated connect aborts as events
- 🐛(frontend) use state instead of a ref for MoreControls container
- 🐛(frontend) stop init_virtual_background from firing on blur updates
+- 🐛(frontend) hoist mute confirmation dialog to VideoConference level
## [1.27.0] - 2026-08-14
diff --git a/src/frontend/src/features/participantTile/components/participantTileFocus/MuteButton.tsx b/src/frontend/src/features/participantTile/components/participantTileFocus/MuteButton.tsx
index 97a7ab12..2c2d1e06 100644
--- a/src/frontend/src/features/participantTile/components/participantTileFocus/MuteButton.tsx
+++ b/src/frontend/src/features/participantTile/components/participantTileFocus/MuteButton.tsx
@@ -1,11 +1,9 @@
import { Participant, Track } from 'livekit-client'
import { useTranslation } from 'react-i18next'
import { useTrackMutedIndicator } from '@livekit/components-react'
-import { useMuteParticipant } from '@/features/rooms/api/muteParticipant'
-import { useState } from 'react'
import { Button } from '@/primitives'
import { RiMicLine, RiMicOffLine } from '@remixicon/react'
-import { MuteAlertDialog } from '@/features/rooms/livekit/components/MuteAlertDialog'
+import { openMuteDialog } from '@/stores/muteDialog'
export const MuteButton = ({ participant }: { participant: Participant }) => {
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
@@ -15,31 +13,18 @@ export const MuteButton = ({ participant }: { participant: Participant }) => {
source: Track.Source.Microphone,
})
- const { muteParticipant } = useMuteParticipant()
- const [isAlertOpen, setIsAlertOpen] = useState(false)
-
const name = participant.name || participant.identity
return (
- <>
-
-
- muteParticipant(participant).then(() => setIsAlertOpen(false))
- }
- onClose={() => setIsAlertOpen(false)}
- name={name}
- />
- >
+
)
}
diff --git a/src/frontend/src/features/participants/components/ParticipantRow.tsx b/src/frontend/src/features/participants/components/ParticipantRow.tsx
index 3e7efc11..93029c3f 100644
--- a/src/frontend/src/features/participants/components/ParticipantRow.tsx
+++ b/src/frontend/src/features/participants/components/ParticipantRow.tsx
@@ -19,13 +19,11 @@ import {
import Source = Track.Source
import { RiMicFill, RiMicOffFill } from '@remixicon/react'
import { Button } from '@/primitives'
-import { useState } from 'react'
-import { useMuteParticipant } from '@/features/rooms/api/muteParticipant'
import { useCanMute } from '@/features/rooms/livekit/hooks/useCanMute'
import { ParticipantMenuButton } from './menu/ParticipantMenuButton'
import { PinBadge } from './PinBadge'
import { UnauthenticatedBadge } from './UnauthenticatedBadge'
-import { MuteAlertDialog } from '@/features/rooms/livekit/components/MuteAlertDialog'
+import { openMuteDialog } from '@/stores/muteDialog'
import { ParticipantName } from './ParticipantName'
type MicIndicatorProps = {
@@ -34,7 +32,6 @@ type MicIndicatorProps = {
const MicIndicator = ({ participant }: MicIndicatorProps) => {
const { t } = useTranslation('rooms')
- const { muteParticipant } = useMuteParticipant()
const { isMuted } = useTrackMutedIndicator({
participant: participant,
source: Source.Microphone,
@@ -42,7 +39,6 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {
const canMute = useCanMute(participant)
const isSpeaking = useIsSpeaking(participant)
- const [isAlertOpen, setIsAlertOpen] = useState(false)
const name = participant.name || participant.identity
const label = isLocal(participant)
@@ -52,46 +48,34 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {
})
return (
- <>
-
-
- muteParticipant(participant).then(() => setIsAlertOpen(false))
- }
- onClose={() => setIsAlertOpen(false)}
- name={name}
- />
- >
+
)
}
diff --git a/src/frontend/src/features/rooms/livekit/components/MuteAlertDialogProvider.tsx b/src/frontend/src/features/rooms/livekit/components/MuteAlertDialogProvider.tsx
new file mode 100644
index 00000000..b03eb688
--- /dev/null
+++ b/src/frontend/src/features/rooms/livekit/components/MuteAlertDialogProvider.tsx
@@ -0,0 +1,28 @@
+import { useRef } from 'react'
+import { useSnapshot } from 'valtio'
+import { useMuteParticipant } from '@/features/rooms/api/muteParticipant'
+import { closeMuteDialog, muteDialogStore } from '@/stores/muteDialog'
+import { MuteAlertDialog } from './MuteAlertDialog'
+
+export const MuteAlertDialogProvider = () => {
+ const { participant } = useSnapshot(muteDialogStore)
+ const { muteParticipant } = useMuteParticipant()
+
+ const lastNameRef = useRef('')
+ if (participant) {
+ lastNameRef.current = participant.name || participant.identity
+ }
+
+ return (
+ {
+ const target = muteDialogStore.participant
+ if (!target) return
+ muteParticipant(target).then(closeMuteDialog)
+ }}
+ />
+ )
+}
diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx
index f470aae7..16084e99 100644
--- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx
+++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx
@@ -19,6 +19,7 @@ import { RoomMetadataSynchronizer } from '../components/RoomMetadataSynchronizer
import { useNoiseReduction } from '../hooks/useNoiseReduction'
import { VideoResolutionSubscription } from '../components/VideoResolutionSubscription'
import { SettingsDialogProvider } from '@/features/settings/components/SettingsDialogProvider'
+import { MuteAlertDialogProvider } from '@/features/rooms/livekit/components/MuteAlertDialogProvider'
import { IsIdleDisconnectModal } from '../components/IsIdleDisconnectModal'
import { ReactionPortals } from '@/features/reactions/components/ReactionPortals'
import { RoomContentArea } from '@/features/layout/components/RoomContentArea'
@@ -144,6 +145,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
+
>
diff --git a/src/frontend/src/stores/muteDialog.ts b/src/frontend/src/stores/muteDialog.ts
new file mode 100644
index 00000000..d4aa4c69
--- /dev/null
+++ b/src/frontend/src/stores/muteDialog.ts
@@ -0,0 +1,18 @@
+import { proxy, ref } from 'valtio'
+import type { Participant } from 'livekit-client'
+
+type State = {
+ participant: Participant | null
+}
+
+export const muteDialogStore = proxy({
+ participant: null,
+})
+
+export const openMuteDialog = (participant: Participant) => {
+ muteDialogStore.participant = ref(participant)
+}
+
+export const closeMuteDialog = () => {
+ muteDialogStore.participant = null
+}