♻️(frontend) introduce a recording provider with clear responsibilities

This component is now extensible and way easier to understand.

Previously, the recording state toast was implicitly acting as a provider,
making its core responsibility unclear for developers. Its role is not to
inject all recording-related elements into the videoconference DOM, but to
expose a clean recording state toast reflecting the current recording status.

This commit also fixes the limit-reached modal that was no longer appearing
after the refactor, ensures the modal is always rendered,
and removes unused React ARIA labels.

In the original code, the limit reached dialog was wrongly rendered
only when the recording state toast was null.
It was a bug in the implementation. Fix it.
This commit is contained in:
lebaudantoine
2026-01-01 00:46:41 +01:00
committed by aleb_the_flash
parent da3dfedcbc
commit 08f281e778
5 changed files with 52 additions and 26 deletions

View File

@@ -2,22 +2,49 @@ import { useTranslation } from 'react-i18next'
import { Button, Dialog, P } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
import { useHumanizeRecordingMaxDuration } from '@/features/recording'
import { useEffect, useState } from 'react'
import { NotificationType } from '@/features/notifications'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import { RoomEvent } from 'livekit-client'
import { decodeNotificationDataReceived } from '@/features/notifications/utils'
import { useRoomContext } from '@livekit/components-react'
export const LimitReachedAlertDialog = () => {
const [isAlertOpen, setIsAlertOpen] = useState(false)
export const LimitReachedAlertDialog = ({
isOpen,
onClose,
}: {
isOpen: boolean
onClose: () => void
}) => {
const { t } = useTranslation('rooms', {
keyPrefix: 'recordingStateToast.limitReachedAlert',
})
const room = useRoomContext()
const isAdminOrOwner = useIsAdminOrOwner()
const maxDuration = useHumanizeRecordingMaxDuration()
useEffect(() => {
const handleDataReceived = (payload: Uint8Array) => {
if (!isAdminOrOwner) return
const notification = decodeNotificationDataReceived(payload)
if (
notification?.type === NotificationType.TranscriptionLimitReached ||
notification?.type === NotificationType.ScreenRecordingLimitReached
) {
setIsAlertOpen(true)
}
}
room.on(RoomEvent.DataReceived, handleDataReceived)
return () => {
room.off(RoomEvent.DataReceived, handleDataReceived)
}
}, [room, isAdminOrOwner])
if (!isAdminOrOwner) return null
return (
<Dialog isOpen={isOpen} role="alertdialog" title={t('title')}>
<Dialog isOpen={isAlertOpen} role="alertdialog" title={t('title')}>
<P>
{t('description', {
duration_message: maxDuration
@@ -28,7 +55,7 @@ export const LimitReachedAlertDialog = ({
})}
</P>
<HStack gap={1}>
<Button variant="text" size="sm" onPress={onClose}>
<Button variant="text" size="sm" onPress={() => setIsAlertOpen(false)}>
{t('button')}
</Button>
</HStack>

View File

@@ -0,0 +1,11 @@
import { LimitReachedAlertDialog } from './LimitReachedAlertDialog'
import { RecordingStateToast } from './RecordingStateToast'
export const RecordingProvider = () => {
return (
<>
<RecordingStateToast />
<LimitReachedAlertDialog />
</>
)
}

View File

@@ -1,7 +1,7 @@
import { css } from '@/styled-system/css'
import { useTranslation } from 'react-i18next'
import { Spinner } from '@/primitives/Spinner'
import { useMemo, useState } from 'react'
import { useMemo } from 'react'
import { Text } from '@/primitives'
import { RiRecordCircleLine } from '@remixicon/react'
import {
@@ -12,8 +12,6 @@ import {
import { FeatureFlags } from '@/features/analytics/enums'
import { Button as RACButton } from 'react-aria-components'
import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import { LimitReachedAlertDialog } from './LimitReachedAlertDialog'
import { useRoomMetadata } from '../hooks/useRoomMetadata'
export const RecordingStateToast = () => {
@@ -21,10 +19,7 @@ export const RecordingStateToast = () => {
keyPrefix: 'recordingStateToast',
})
const isAdminOrOwner = useIsAdminOrOwner()
const { openTranscript, openScreenRecording } = useSidePanel()
const [isAlertOpen, setIsAlertOpen] = useState(false)
const hasTranscriptAccess = useHasRecordingAccess(
RecordingMode.Transcript,
@@ -65,14 +60,7 @@ export const RecordingStateToast = () => {
return `${metadata.recording_mode}.${metadata.recording_status}`
}, [metadata, isStarted, isStarting])
if (!key)
return isAdminOrOwner ? (
<LimitReachedAlertDialog
isOpen={isAlertOpen}
onClose={() => setIsAlertOpen(false)}
aria-label="Recording limit exceeded"
/>
) : null
if (!key) return null
const hasScreenRecordingAccessAndActive =
isScreenRecordingActive && hasScreenRecordingAccess

View File

@@ -11,7 +11,7 @@ export { useStopRecording } from './api/stopRecording'
export { RecordingMode, RecordingStatus } from './types'
// components
export { RecordingStateToast } from './components/RecordingStateToast'
export { RecordingProvider } from './components/RecordingProvider'
export { TranscriptSidePanel } from './components/TranscriptSidePanel'
export { ScreenRecordingSidePanel } from './components/ScreenRecordingSidePanel'

View File

@@ -26,7 +26,7 @@ import { FocusLayout } from '../components/FocusLayout'
import { ParticipantTile } from '../components/ParticipantTile'
import { SidePanel } from '../components/SidePanel'
import { useSidePanel } from '../hooks/useSidePanel'
import { RecordingStateToast } from '@/features/recording'
import { RecordingProvider } from '@/features/recording'
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
import { useConnectionObserver } from '../hooks/useConnectionObserver'
import { useNoiseReduction } from '../hooks/useNoiseReduction'
@@ -261,7 +261,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
)}
<RoomAudioRenderer />
<ConnectionStateToast />
<RecordingStateToast />
<RecordingProvider />
<SettingsDialogProvider />
</div>
)