From 08f281e7786c115c0f256eeda28ed7af51f83de6 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 1 Jan 2026 00:46:41 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20introduce=20a=20?= =?UTF-8?q?recording=20provider=20with=20clear=20responsibilities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../components/LimitReachedAlertDialog.tsx | 45 +++++++++++++++---- .../components/RecordingProvider.tsx | 11 +++++ .../components/RecordingStateToast.tsx | 16 +------ src/frontend/src/features/recording/index.ts | 2 +- .../rooms/livekit/prefabs/VideoConference.tsx | 4 +- 5 files changed, 52 insertions(+), 26 deletions(-) create mode 100644 src/frontend/src/features/recording/components/RecordingProvider.tsx diff --git a/src/frontend/src/features/recording/components/LimitReachedAlertDialog.tsx b/src/frontend/src/features/recording/components/LimitReachedAlertDialog.tsx index 65b57394..ad2481e5 100644 --- a/src/frontend/src/features/recording/components/LimitReachedAlertDialog.tsx +++ b/src/frontend/src/features/recording/components/LimitReachedAlertDialog.tsx @@ -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 ( - +

{t('description', { duration_message: maxDuration @@ -28,7 +55,7 @@ export const LimitReachedAlertDialog = ({ })}

- diff --git a/src/frontend/src/features/recording/components/RecordingProvider.tsx b/src/frontend/src/features/recording/components/RecordingProvider.tsx new file mode 100644 index 00000000..61f13098 --- /dev/null +++ b/src/frontend/src/features/recording/components/RecordingProvider.tsx @@ -0,0 +1,11 @@ +import { LimitReachedAlertDialog } from './LimitReachedAlertDialog' +import { RecordingStateToast } from './RecordingStateToast' + +export const RecordingProvider = () => { + return ( + <> + + + + ) +} diff --git a/src/frontend/src/features/recording/components/RecordingStateToast.tsx b/src/frontend/src/features/recording/components/RecordingStateToast.tsx index 5c084268..a29bd4f4 100644 --- a/src/frontend/src/features/recording/components/RecordingStateToast.tsx +++ b/src/frontend/src/features/recording/components/RecordingStateToast.tsx @@ -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 ? ( - setIsAlertOpen(false)} - aria-label="Recording limit exceeded" - /> - ) : null + if (!key) return null const hasScreenRecordingAccessAndActive = isScreenRecordingActive && hasScreenRecordingAccess diff --git a/src/frontend/src/features/recording/index.ts b/src/frontend/src/features/recording/index.ts index a0b0bcee..a1e590c2 100644 --- a/src/frontend/src/features/recording/index.ts +++ b/src/frontend/src/features/recording/index.ts @@ -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' diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index edffa7e3..cee1eece 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -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) { )} - + )