diff --git a/src/frontend/src/features/recording/components/ControlsButton.tsx b/src/frontend/src/features/recording/components/ControlsButton.tsx
new file mode 100644
index 00000000..6b31c593
--- /dev/null
+++ b/src/frontend/src/features/recording/components/ControlsButton.tsx
@@ -0,0 +1,116 @@
+import { css } from '@/styled-system/css'
+import { HStack } from '@/styled-system/jsx'
+import { Spinner } from '@/primitives/Spinner'
+import { Button, Text } from '@/primitives'
+import { useTranslation } from 'react-i18next'
+import { RecordingStatuses } from '../hooks/useRecordingStatuses'
+import { ReactNode, useEffect, useRef, useState } from 'react'
+import { useRoomContext } from '@livekit/components-react'
+import { ConnectionState } from 'livekit-client'
+
+const Layout = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+)
+
+interface ControlsButtonProps {
+ i18nKeyPrefix: string
+ statuses: RecordingStatuses
+ handle: () => void
+ isPendingToStart: boolean
+ isPendingToStop: boolean
+}
+
+const MIN_SPINNER_DISPLAY_TIME = 2000
+
+export const ControlsButton = ({
+ i18nKeyPrefix,
+ statuses,
+ handle,
+ isPendingToStart,
+ isPendingToStop,
+}: ControlsButtonProps) => {
+ const { t } = useTranslation('rooms', { keyPrefix: i18nKeyPrefix })
+
+ const room = useRoomContext()
+ const isRoomConnected = room.state == ConnectionState.Connected
+
+ const [showSaving, setShowSaving] = useState(false)
+ const timeoutRef = useRef()
+
+ const isSaving = statuses.isSaving || isPendingToStop
+ const isDisabled = !isRoomConnected
+
+ useEffect(() => {
+ if (isSaving) {
+ clearTimeout(timeoutRef.current)
+ setShowSaving(true)
+ } else if (showSaving) {
+ timeoutRef.current = setTimeout(() => {
+ setShowSaving(false)
+ }, MIN_SPINNER_DISPLAY_TIME)
+ }
+
+ return () => clearTimeout(timeoutRef.current)
+ }, [isSaving, showSaving])
+
+ // Saving state
+ if (showSaving) {
+ return (
+
+
+
+ {t('button.saving')}
+
+
+ )
+ }
+
+ // Starting state
+ if (statuses.isStarting || isPendingToStart) {
+ return (
+
+
+
+ {t('button.starting')}
+
+
+ )
+ }
+
+ // Active state (Stop button)
+ if (statuses.isStarted) {
+ return (
+
+
+
+ )
+ }
+
+ // Inactive state (Start button)
+ return (
+
+
+
+ )
+}
diff --git a/src/frontend/src/features/recording/components/RecordingStateToast.tsx b/src/frontend/src/features/recording/components/RecordingStateToast.tsx
index 8bdbee1f..5c084268 100644
--- a/src/frontend/src/features/recording/components/RecordingStateToast.tsx
+++ b/src/frontend/src/features/recording/components/RecordingStateToast.tsx
@@ -1,132 +1,69 @@
import { css } from '@/styled-system/css'
import { useTranslation } from 'react-i18next'
-import { useSnapshot } from 'valtio'
-import { useRoomContext } from '@livekit/components-react'
import { Spinner } from '@/primitives/Spinner'
-import { useEffect, useMemo, useState } from 'react'
+import { useMemo, useState } from 'react'
import { Text } from '@/primitives'
-import { RoomEvent } from 'livekit-client'
-import { decodeNotificationDataReceived } from '@/features/notifications/utils'
-import { NotificationType } from '@/features/notifications/NotificationType'
-import { RecordingStatus, recordingStore } from '@/stores/recording'
import { RiRecordCircleLine } from '@remixicon/react'
import {
RecordingMode,
useHasRecordingAccess,
- useIsRecordingActive,
+ useRecordingStatuses,
} from '@/features/recording'
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 = () => {
const { t } = useTranslation('rooms', {
keyPrefix: 'recordingStateToast',
})
- const room = useRoomContext()
+
const isAdminOrOwner = useIsAdminOrOwner()
const { openTranscript, openScreenRecording } = useSidePanel()
const [isAlertOpen, setIsAlertOpen] = useState(false)
- const recordingSnap = useSnapshot(recordingStore)
-
const hasTranscriptAccess = useHasRecordingAccess(
RecordingMode.Transcript,
FeatureFlags.Transcript
)
- const isTranscriptActive = useIsRecordingActive(RecordingMode.Transcript)
-
const hasScreenRecordingAccess = useHasRecordingAccess(
RecordingMode.ScreenRecording,
FeatureFlags.ScreenRecording
)
- const isScreenRecordingActive = useIsRecordingActive(
- RecordingMode.ScreenRecording
- )
+ const {
+ isStarted: isScreenRecordingStarted,
+ isStarting: isScreenRecordingStarting,
+ isActive: isScreenRecordingActive,
+ } = useRecordingStatuses(RecordingMode.ScreenRecording)
- useEffect(() => {
- if (room.isRecording && recordingSnap.status == RecordingStatus.STOPPED) {
- recordingStore.status = RecordingStatus.ANY_STARTED
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [room.isRecording])
+ const {
+ isStarted: isTranscriptStarted,
+ isStarting: isTranscriptStarting,
+ isActive: isTranscriptActive,
+ } = useRecordingStatuses(RecordingMode.Transcript)
- useEffect(() => {
- const handleDataReceived = (payload: Uint8Array) => {
- const notification = decodeNotificationDataReceived(payload)
+ const isStarted = isScreenRecordingStarted || isTranscriptStarted
+ const isStarting = isTranscriptStarting || isScreenRecordingStarting
- if (!notification) return
-
- switch (notification.type) {
- case NotificationType.TranscriptionStarted:
- recordingStore.status = RecordingStatus.TRANSCRIPT_STARTING
- break
- case NotificationType.TranscriptionStopped:
- recordingStore.status = RecordingStatus.TRANSCRIPT_STOPPING
- break
- case NotificationType.TranscriptionLimitReached:
- if (isAdminOrOwner) setIsAlertOpen(true)
- recordingStore.status = RecordingStatus.TRANSCRIPT_STOPPING
- break
- case NotificationType.ScreenRecordingStarted:
- recordingStore.status = RecordingStatus.SCREEN_RECORDING_STARTING
- break
- case NotificationType.ScreenRecordingStopped:
- recordingStore.status = RecordingStatus.SCREEN_RECORDING_STOPPING
- break
- case NotificationType.ScreenRecordingLimitReached:
- if (isAdminOrOwner) setIsAlertOpen(true)
- recordingStore.status = RecordingStatus.SCREEN_RECORDING_STOPPING
- break
- default:
- return
- }
- }
-
- const handleRecordingStatusChanged = (status: boolean) => {
- if (!status) {
- recordingStore.status = RecordingStatus.STOPPED
- } else if (recordingSnap.status == RecordingStatus.TRANSCRIPT_STARTING) {
- recordingStore.status = RecordingStatus.TRANSCRIPT_STARTED
- } else if (
- recordingSnap.status == RecordingStatus.SCREEN_RECORDING_STARTING
- ) {
- recordingStore.status = RecordingStatus.SCREEN_RECORDING_STARTED
- } else {
- recordingStore.status = RecordingStatus.ANY_STARTED
- }
- }
-
- room.on(RoomEvent.DataReceived, handleDataReceived)
- room.on(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
-
- return () => {
- room.off(RoomEvent.DataReceived, handleDataReceived)
- room.off(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
- }
- }, [room, recordingSnap, setIsAlertOpen, isAdminOrOwner])
+ const metadata = useRoomMetadata()
const key = useMemo(() => {
- switch (recordingSnap.status) {
- case RecordingStatus.TRANSCRIPT_STARTED:
- return 'transcript.started'
- case RecordingStatus.TRANSCRIPT_STARTING:
- return 'transcript.starting'
- case RecordingStatus.SCREEN_RECORDING_STARTED:
- return 'screenRecording.started'
- case RecordingStatus.SCREEN_RECORDING_STARTING:
- return 'screenRecording.starting'
- case RecordingStatus.ANY_STARTED:
- return 'any.started'
- default:
- return
+ if (!metadata?.recording_status || !metadata?.recording_mode) {
+ return undefined
}
- }, [recordingSnap])
+
+ if (!isStarting && !isStarted) {
+ return undefined
+ }
+
+ return `${metadata.recording_mode}.${metadata.recording_status}`
+ }, [metadata, isStarted, isStarting])
if (!key)
return isAdminOrOwner ? (
@@ -137,8 +74,6 @@ export const RecordingStateToast = () => {
/>
) : null
- const isStarted = key?.includes('started')
-
const hasScreenRecordingAccessAndActive =
isScreenRecordingActive && hasScreenRecordingAccess
const hasTranscriptAccessAndActive = isTranscriptActive && hasTranscriptAccess
diff --git a/src/frontend/src/features/recording/components/ScreenRecordingSidePanel.tsx b/src/frontend/src/features/recording/components/ScreenRecordingSidePanel.tsx
index 00310f55..26deb918 100644
--- a/src/frontend/src/features/recording/components/ScreenRecordingSidePanel.tsx
+++ b/src/frontend/src/features/recording/components/ScreenRecordingSidePanel.tsx
@@ -9,11 +9,10 @@ import {
useStartRecording,
useStopRecording,
useHumanizeRecordingMaxDuration,
+ useRecordingStatuses,
} from '@/features/recording'
-import { useEffect, useMemo, useState } from 'react'
-import { ConnectionState, RoomEvent } from 'livekit-client'
+import { useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { RecordingStatus, recordingStore } from '@/stores/recording'
import {
NotificationType,
@@ -21,13 +20,12 @@ import {
useNotifyParticipants,
} from '@/features/notifications'
import posthog from 'posthog-js'
-import { useSnapshot } from 'valtio/index'
-import { Spinner } from '@/primitives/Spinner'
import { useConfig } from '@/api/useConfig'
import { FeatureFlags } from '@/features/analytics/enums'
import { NoAccessView } from './NoAccessView'
-import { HStack, VStack } from '@/styled-system/jsx'
+import { ControlsButton } from './ControlsButton'
import { RowWrapper } from './RowWrapper'
+import { VStack } from '@/styled-system/jsx'
import { Checkbox } from '@/primitives/Checkbox'
import { useTranscriptionLanguage } from '@/features/settings'
@@ -35,9 +33,8 @@ export const ScreenRecordingSidePanel = () => {
const { data } = useConfig()
const recordingMaxDuration = useHumanizeRecordingMaxDuration()
- const [isLoading, setIsLoading] = useState(false)
- const recordingSnap = useSnapshot(recordingStore)
- const { t } = useTranslation('rooms', { keyPrefix: 'screenRecording' })
+ const keyPrefix = 'screenRecording'
+ const { t } = useTranslation('rooms', { keyPrefix })
const [isErrorDialogOpen, setIsErrorDialogOpen] = useState('')
@@ -63,31 +60,9 @@ export const ScreenRecordingSidePanel = () => {
onError: () => setIsErrorDialogOpen('stop'),
})
- const statuses = useMemo(() => {
- return {
- isAnotherModeStarted:
- recordingSnap.status == RecordingStatus.TRANSCRIPT_STARTED,
- isStarting:
- recordingSnap.status == RecordingStatus.SCREEN_RECORDING_STARTING,
- isStarted:
- recordingSnap.status == RecordingStatus.SCREEN_RECORDING_STARTED,
- isStopping:
- recordingSnap.status == RecordingStatus.SCREEN_RECORDING_STOPPING,
- }
- }, [recordingSnap])
+ const statuses = useRecordingStatuses(RecordingMode.ScreenRecording)
const room = useRoomContext()
- const isRoomConnected = room.state == ConnectionState.Connected
-
- useEffect(() => {
- const handleRecordingStatusChanged = () => {
- setIsLoading(false)
- }
- room.on(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
- return () => {
- room.off(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
- }
- }, [room])
const handleScreenRecording = async () => {
if (!roomId) {
@@ -95,11 +70,10 @@ export const ScreenRecordingSidePanel = () => {
return
}
try {
- setIsLoading(true)
- if (room.isRecording) {
+ if (statuses.isStarted || statuses.isStarting) {
setIncludeTranscript(false)
await stopRecordingRoom({ id: roomId })
- recordingStore.status = RecordingStatus.SCREEN_RECORDING_STOPPING
+
await notifyParticipants({
type: NotificationType.ScreenRecordingStopped,
})
@@ -120,7 +94,7 @@ export const ScreenRecordingSidePanel = () => {
mode: RecordingMode.ScreenRecording,
options: recordingOptions,
})
- recordingStore.status = RecordingStatus.SCREEN_RECORDING_STARTING
+
await notifyParticipants({
type: NotificationType.ScreenRecordingStarted,
})
@@ -128,14 +102,13 @@ export const ScreenRecordingSidePanel = () => {
}
} catch (error) {
console.error('Failed to handle recording:', error)
- setIsLoading(false)
}
}
if (hasFeatureWithoutAdminRights) {
return (
{
size="sm"
isSelected={includeTranscript}
onChange={setIncludeTranscript}
- isDisabled={
- statuses.isStarting || statuses.isStarted || isPendingToStart
- }
+ isDisabled={statuses.isActive || isPendingToStart}
>
{t('details.transcription')}
-
- {statuses.isStopping || isPendingToStop ? (
-
-
- {t('button.saving')}
-
- ) : (
- <>
- {statuses.isStarted || statuses.isStarting || room.isRecording ? (
-
- ) : (
-
- )}
- >
- )}
-
+