(frontend) update recording metadata alongside recording state changes

Following the previous commit, refactor the frontend to rely on room metadata to
track which recording is running and update the interface accordingly. This
implementation is not fully functional yet.

The limit-reached dialog triggering mechanism is currently broken and will be
fixed in upcoming commits. I also simplified the interface lifecycle, but some
edge cases are not yet handled—for example, transcription controls should be
disabled when a screen recording is started. This will be improved soon.

Controls were extracted into a reusable component using early returns. This
makes the logic easier to read, but slightly increases the overall complexity of
the recording side panel component.

Relying on literals to manage recording statuses is quite poor, feel free to
enhance this part.
This commit is contained in:
lebaudantoine
2026-01-01 00:25:06 +01:00
committed by aleb_the_flash
parent 16badde82d
commit da3dfedcbc
13 changed files with 258 additions and 295 deletions

View File

@@ -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 }) => (
<div
className={css({
marginBottom: '80px',
width: '100%',
})}
>
{children}
</div>
)
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<NodeJS.Timeout>()
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 (
<Layout>
<HStack width="100%" height="46px" justify="center">
<Spinner size={30} />
<Text variant="body">{t('button.saving')}</Text>
</HStack>
</Layout>
)
}
// Starting state
if (statuses.isStarting || isPendingToStart) {
return (
<Layout>
<HStack width="100%" height="46px" justify="center">
<Spinner size={30} />
{t('button.starting')}
</HStack>
</Layout>
)
}
// Active state (Stop button)
if (statuses.isStarted) {
return (
<Layout>
<Button
variant="tertiary"
fullWidth
onPress={handle}
isDisabled={isDisabled}
>
{t('button.stop')}
</Button>
</Layout>
)
}
// Inactive state (Start button)
return (
<Layout>
<Button
variant="tertiary"
fullWidth
onPress={handle}
isDisabled={isDisabled}
>
{t('button.start')}
</Button>
</Layout>
)
}

View File

@@ -1,132 +1,69 @@
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSnapshot } from 'valtio'
import { useRoomContext } from '@livekit/components-react'
import { Spinner } from '@/primitives/Spinner' import { Spinner } from '@/primitives/Spinner'
import { useEffect, useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { Text } from '@/primitives' 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 { RiRecordCircleLine } from '@remixicon/react'
import { import {
RecordingMode, RecordingMode,
useHasRecordingAccess, useHasRecordingAccess,
useIsRecordingActive, useRecordingStatuses,
} from '@/features/recording' } from '@/features/recording'
import { FeatureFlags } from '@/features/analytics/enums' import { FeatureFlags } from '@/features/analytics/enums'
import { Button as RACButton } from 'react-aria-components' import { Button as RACButton } from 'react-aria-components'
import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel' import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner' import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import { LimitReachedAlertDialog } from './LimitReachedAlertDialog' import { LimitReachedAlertDialog } from './LimitReachedAlertDialog'
import { useRoomMetadata } from '../hooks/useRoomMetadata'
export const RecordingStateToast = () => { export const RecordingStateToast = () => {
const { t } = useTranslation('rooms', { const { t } = useTranslation('rooms', {
keyPrefix: 'recordingStateToast', keyPrefix: 'recordingStateToast',
}) })
const room = useRoomContext()
const isAdminOrOwner = useIsAdminOrOwner() const isAdminOrOwner = useIsAdminOrOwner()
const { openTranscript, openScreenRecording } = useSidePanel() const { openTranscript, openScreenRecording } = useSidePanel()
const [isAlertOpen, setIsAlertOpen] = useState(false) const [isAlertOpen, setIsAlertOpen] = useState(false)
const recordingSnap = useSnapshot(recordingStore)
const hasTranscriptAccess = useHasRecordingAccess( const hasTranscriptAccess = useHasRecordingAccess(
RecordingMode.Transcript, RecordingMode.Transcript,
FeatureFlags.Transcript FeatureFlags.Transcript
) )
const isTranscriptActive = useIsRecordingActive(RecordingMode.Transcript)
const hasScreenRecordingAccess = useHasRecordingAccess( const hasScreenRecordingAccess = useHasRecordingAccess(
RecordingMode.ScreenRecording, RecordingMode.ScreenRecording,
FeatureFlags.ScreenRecording FeatureFlags.ScreenRecording
) )
const isScreenRecordingActive = useIsRecordingActive( const {
RecordingMode.ScreenRecording isStarted: isScreenRecordingStarted,
) isStarting: isScreenRecordingStarting,
isActive: isScreenRecordingActive,
} = useRecordingStatuses(RecordingMode.ScreenRecording)
useEffect(() => { const {
if (room.isRecording && recordingSnap.status == RecordingStatus.STOPPED) { isStarted: isTranscriptStarted,
recordingStore.status = RecordingStatus.ANY_STARTED isStarting: isTranscriptStarting,
} isActive: isTranscriptActive,
// eslint-disable-next-line react-hooks/exhaustive-deps } = useRecordingStatuses(RecordingMode.Transcript)
}, [room.isRecording])
useEffect(() => { const isStarted = isScreenRecordingStarted || isTranscriptStarted
const handleDataReceived = (payload: Uint8Array) => { const isStarting = isTranscriptStarting || isScreenRecordingStarting
const notification = decodeNotificationDataReceived(payload)
if (!notification) return const metadata = useRoomMetadata()
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 key = useMemo(() => { const key = useMemo(() => {
switch (recordingSnap.status) { if (!metadata?.recording_status || !metadata?.recording_mode) {
case RecordingStatus.TRANSCRIPT_STARTED: return undefined
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
} }
}, [recordingSnap])
if (!isStarting && !isStarted) {
return undefined
}
return `${metadata.recording_mode}.${metadata.recording_status}`
}, [metadata, isStarted, isStarting])
if (!key) if (!key)
return isAdminOrOwner ? ( return isAdminOrOwner ? (
@@ -137,8 +74,6 @@ export const RecordingStateToast = () => {
/> />
) : null ) : null
const isStarted = key?.includes('started')
const hasScreenRecordingAccessAndActive = const hasScreenRecordingAccessAndActive =
isScreenRecordingActive && hasScreenRecordingAccess isScreenRecordingActive && hasScreenRecordingAccess
const hasTranscriptAccessAndActive = isTranscriptActive && hasTranscriptAccess const hasTranscriptAccessAndActive = isTranscriptActive && hasTranscriptAccess

View File

@@ -9,11 +9,10 @@ import {
useStartRecording, useStartRecording,
useStopRecording, useStopRecording,
useHumanizeRecordingMaxDuration, useHumanizeRecordingMaxDuration,
useRecordingStatuses,
} from '@/features/recording' } from '@/features/recording'
import { useEffect, useMemo, useState } from 'react' import { useState } from 'react'
import { ConnectionState, RoomEvent } from 'livekit-client'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RecordingStatus, recordingStore } from '@/stores/recording'
import { import {
NotificationType, NotificationType,
@@ -21,13 +20,12 @@ import {
useNotifyParticipants, useNotifyParticipants,
} from '@/features/notifications' } from '@/features/notifications'
import posthog from 'posthog-js' import posthog from 'posthog-js'
import { useSnapshot } from 'valtio/index'
import { Spinner } from '@/primitives/Spinner'
import { useConfig } from '@/api/useConfig' import { useConfig } from '@/api/useConfig'
import { FeatureFlags } from '@/features/analytics/enums' import { FeatureFlags } from '@/features/analytics/enums'
import { NoAccessView } from './NoAccessView' import { NoAccessView } from './NoAccessView'
import { HStack, VStack } from '@/styled-system/jsx' import { ControlsButton } from './ControlsButton'
import { RowWrapper } from './RowWrapper' import { RowWrapper } from './RowWrapper'
import { VStack } from '@/styled-system/jsx'
import { Checkbox } from '@/primitives/Checkbox' import { Checkbox } from '@/primitives/Checkbox'
import { useTranscriptionLanguage } from '@/features/settings' import { useTranscriptionLanguage } from '@/features/settings'
@@ -35,9 +33,8 @@ export const ScreenRecordingSidePanel = () => {
const { data } = useConfig() const { data } = useConfig()
const recordingMaxDuration = useHumanizeRecordingMaxDuration() const recordingMaxDuration = useHumanizeRecordingMaxDuration()
const [isLoading, setIsLoading] = useState(false) const keyPrefix = 'screenRecording'
const recordingSnap = useSnapshot(recordingStore) const { t } = useTranslation('rooms', { keyPrefix })
const { t } = useTranslation('rooms', { keyPrefix: 'screenRecording' })
const [isErrorDialogOpen, setIsErrorDialogOpen] = useState('') const [isErrorDialogOpen, setIsErrorDialogOpen] = useState('')
@@ -63,31 +60,9 @@ export const ScreenRecordingSidePanel = () => {
onError: () => setIsErrorDialogOpen('stop'), onError: () => setIsErrorDialogOpen('stop'),
}) })
const statuses = useMemo(() => { const statuses = useRecordingStatuses(RecordingMode.ScreenRecording)
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 room = useRoomContext() 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 () => { const handleScreenRecording = async () => {
if (!roomId) { if (!roomId) {
@@ -95,11 +70,10 @@ export const ScreenRecordingSidePanel = () => {
return return
} }
try { try {
setIsLoading(true) if (statuses.isStarted || statuses.isStarting) {
if (room.isRecording) {
setIncludeTranscript(false) setIncludeTranscript(false)
await stopRecordingRoom({ id: roomId }) await stopRecordingRoom({ id: roomId })
recordingStore.status = RecordingStatus.SCREEN_RECORDING_STOPPING
await notifyParticipants({ await notifyParticipants({
type: NotificationType.ScreenRecordingStopped, type: NotificationType.ScreenRecordingStopped,
}) })
@@ -120,7 +94,7 @@ export const ScreenRecordingSidePanel = () => {
mode: RecordingMode.ScreenRecording, mode: RecordingMode.ScreenRecording,
options: recordingOptions, options: recordingOptions,
}) })
recordingStore.status = RecordingStatus.SCREEN_RECORDING_STARTING
await notifyParticipants({ await notifyParticipants({
type: NotificationType.ScreenRecordingStarted, type: NotificationType.ScreenRecordingStarted,
}) })
@@ -128,14 +102,13 @@ export const ScreenRecordingSidePanel = () => {
} }
} catch (error) { } catch (error) {
console.error('Failed to handle recording:', error) console.error('Failed to handle recording:', error)
setIsLoading(false)
} }
} }
if (hasFeatureWithoutAdminRights) { if (hasFeatureWithoutAdminRights) {
return ( return (
<NoAccessView <NoAccessView
i18nKeyPrefix="screenRecording" i18nKeyPrefix={keyPrefix}
i18nKey="notAdminOrOwner" i18nKey="notAdminOrOwner"
helpArticle={data?.support?.help_article_recording} helpArticle={data?.support?.help_article_recording}
imagePath="/assets/intro-slider/4.png" imagePath="/assets/intro-slider/4.png"
@@ -208,51 +181,19 @@ export const ScreenRecordingSidePanel = () => {
size="sm" size="sm"
isSelected={includeTranscript} isSelected={includeTranscript}
onChange={setIncludeTranscript} onChange={setIncludeTranscript}
isDisabled={ isDisabled={statuses.isActive || isPendingToStart}
statuses.isStarting || statuses.isStarted || isPendingToStart
}
> >
<Text variant="sm">{t('details.transcription')}</Text> <Text variant="sm">{t('details.transcription')}</Text>
</Checkbox> </Checkbox>
</div> </div>
</VStack> </VStack>
<div <ControlsButton
className={css({ i18nKeyPrefix={keyPrefix}
marginBottom: '80px', handle={handleScreenRecording}
width: '100%', statuses={statuses}
})} isPendingToStart={isPendingToStart}
> isPendingToStop={isPendingToStop}
{statuses.isStopping || isPendingToStop ? ( />
<HStack width={'100%'} height={'46px'} justify="center">
<Spinner size={30} />
<Text variant="body">{t('button.saving')}</Text>
</HStack>
) : (
<>
{statuses.isStarted || statuses.isStarting || room.isRecording ? (
<Button
variant="tertiary"
fullWidth
onPress={() => handleScreenRecording()}
isDisabled={statuses.isStopping || isPendingToStop || isLoading}
data-attr="stop-transcript"
>
{t('button.stop')}
</Button>
) : (
<Button
variant="tertiary"
fullWidth
onPress={() => handleScreenRecording()}
isDisabled={isPendingToStart || !isRoomConnected || isLoading}
data-attr="start-transcript"
>
{t('button.start')}
</Button>
)}
</>
)}
</div>
<Dialog <Dialog
isOpen={!!isErrorDialogOpen} isOpen={!!isErrorDialogOpen}
role="alertdialog" role="alertdialog"

View File

@@ -10,11 +10,10 @@ import {
useStopRecording, useStopRecording,
useHasFeatureWithoutAdminRights, useHasFeatureWithoutAdminRights,
useHumanizeRecordingMaxDuration, useHumanizeRecordingMaxDuration,
useRecordingStatuses,
} from '../index' } from '../index'
import { useEffect, useMemo, useState } from 'react' import { useState } from 'react'
import { ConnectionState, RoomEvent } from 'livekit-client'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RecordingStatus } from '@store/recording'
import { FeatureFlags } from '@/features/analytics/enums' import { FeatureFlags } from '@/features/analytics/enums'
import { import {
NotificationType, NotificationType,
@@ -22,8 +21,6 @@ import {
notifyRecordingSaveInProgress, notifyRecordingSaveInProgress,
} from '@/features/notifications' } from '@/features/notifications'
import posthog from 'posthog-js' import posthog from 'posthog-js'
import { useSnapshot } from 'valtio/index'
import { Spinner } from '@/primitives/Spinner'
import { useConfig } from '@/api/useConfig' import { useConfig } from '@/api/useConfig'
import { VStack } from '@/styled-system/jsx' import { VStack } from '@/styled-system/jsx'
import { Checkbox } from '@/primitives/Checkbox.tsx' import { Checkbox } from '@/primitives/Checkbox.tsx'
@@ -34,20 +31,19 @@ import {
useTranscriptionLanguage, useTranscriptionLanguage,
} from '@/features/settings' } from '@/features/settings'
import { NoAccessView } from './NoAccessView' import { NoAccessView } from './NoAccessView'
import { ControlsButton } from './ControlsButton'
import { RowWrapper } from './RowWrapper' import { RowWrapper } from './RowWrapper'
export const TranscriptSidePanel = () => { export const TranscriptSidePanel = () => {
const { data } = useConfig() const { data } = useConfig()
const recordingMaxDuration = useHumanizeRecordingMaxDuration() const recordingMaxDuration = useHumanizeRecordingMaxDuration()
const [isLoading, setIsLoading] = useState(false) const keyPrefix = 'transcript'
const { t } = useTranslation('rooms', { keyPrefix: 'transcript' }) const { t } = useTranslation('rooms', { keyPrefix })
const [isErrorDialogOpen, setIsErrorDialogOpen] = useState('') const [isErrorDialogOpen, setIsErrorDialogOpen] = useState('')
const [includeScreenRecording, setIncludeScreenRecording] = useState(false) const [includeScreenRecording, setIncludeScreenRecording] = useState(false)
const recordingSnap = useSnapshot(recordingStore)
const { notifyParticipants } = useNotifyParticipants() const { notifyParticipants } = useNotifyParticipants()
const { selectedLanguageKey, selectedLanguageLabel, isLanguageSetToAuto } = const { selectedLanguageKey, selectedLanguageLabel, isLanguageSetToAuto } =
useTranscriptionLanguage() useTranscriptionLanguage()
@@ -76,28 +72,9 @@ export const TranscriptSidePanel = () => {
onError: () => setIsErrorDialogOpen('stop'), onError: () => setIsErrorDialogOpen('stop'),
}) })
const statuses = useMemo(() => { const statuses = useRecordingStatuses(RecordingMode.Transcript)
return {
isAnotherModeStarted:
recordingSnap.status == RecordingStatus.SCREEN_RECORDING_STARTED,
isStarting: recordingSnap.status == RecordingStatus.TRANSCRIPT_STARTING,
isStarted: recordingSnap.status == RecordingStatus.TRANSCRIPT_STARTED,
isStopping: recordingSnap.status == RecordingStatus.TRANSCRIPT_STOPPING,
}
}, [recordingSnap])
const room = useRoomContext() 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 handleTranscript = async () => { const handleTranscript = async () => {
if (!roomId) { if (!roomId) {
@@ -105,11 +82,10 @@ export const TranscriptSidePanel = () => {
return return
} }
try { try {
setIsLoading(true) if (statuses.isStarted || statuses.isStarting) {
if (room.isRecording) {
await stopRecordingRoom({ id: roomId }) await stopRecordingRoom({ id: roomId })
setIncludeScreenRecording(false) setIncludeScreenRecording(false)
recordingStore.status = RecordingStatus.TRANSCRIPT_STOPPING
await notifyParticipants({ await notifyParticipants({
type: NotificationType.TranscriptionStopped, type: NotificationType.TranscriptionStopped,
}) })
@@ -126,8 +102,10 @@ export const TranscriptSidePanel = () => {
...(!isLanguageSetToAuto && { ...(!isLanguageSetToAuto && {
language: selectedLanguageKey, language: selectedLanguageKey,
}), }),
...(includeScreenRecording && {
transcribe: true,
original_mode: RecordingMode.Transcript,
}), }),
...(includeScreenRecording && { transcribe: true }),
} }
await startRecordingRoom({ await startRecordingRoom({
@@ -135,7 +113,7 @@ export const TranscriptSidePanel = () => {
mode: recordingMode, mode: recordingMode,
options: recordingOptions, options: recordingOptions,
}) })
recordingStore.status = RecordingStatus.TRANSCRIPT_STARTING
await notifyParticipants({ await notifyParticipants({
type: NotificationType.TranscriptionStarted, type: NotificationType.TranscriptionStarted,
}) })
@@ -143,14 +121,13 @@ export const TranscriptSidePanel = () => {
} }
} catch (error) { } catch (error) {
console.error('Failed to handle transcript:', error) console.error('Failed to handle transcript:', error)
setIsLoading(false)
} }
} }
if (hasFeatureWithoutAdminRights) { if (hasFeatureWithoutAdminRights) {
return ( return (
<NoAccessView <NoAccessView
i18nKeyPrefix="transcript" i18nKeyPrefix={keyPrefix}
i18nKey="notAdminOrOwner" i18nKey="notAdminOrOwner"
helpArticle={data?.support?.help_article_transcript} helpArticle={data?.support?.help_article_transcript}
imagePath="/assets/intro-slider/3.png" imagePath="/assets/intro-slider/3.png"
@@ -161,7 +138,7 @@ export const TranscriptSidePanel = () => {
if (!hasTranscriptAccess) { if (!hasTranscriptAccess) {
return ( return (
<NoAccessView <NoAccessView
i18nKeyPrefix="transcript" i18nKeyPrefix={keyPrefix}
i18nKey="premium" i18nKey="premium"
helpArticle={data?.support?.help_article_transcript} helpArticle={data?.support?.help_article_transcript}
imagePath="/assets/intro-slider/3.png" imagePath="/assets/intro-slider/3.png"
@@ -251,7 +228,6 @@ export const TranscriptSidePanel = () => {
</Text> </Text>
</RowWrapper> </RowWrapper>
<div className={css({ height: '15px' })} /> <div className={css({ height: '15px' })} />
<div <div
className={css({ className={css({
width: '100%', width: '100%',
@@ -262,51 +238,19 @@ export const TranscriptSidePanel = () => {
size="sm" size="sm"
isSelected={includeScreenRecording} isSelected={includeScreenRecording}
onChange={setIncludeScreenRecording} onChange={setIncludeScreenRecording}
isDisabled={ isDisabled={statuses.isActive || isPendingToStart}
statuses.isStarting || statuses.isStarted || isPendingToStart
}
> >
<Text variant="sm">{t('details.recording')}</Text> <Text variant="sm">{t('details.recording')}</Text>
</Checkbox> </Checkbox>
</div> </div>
</VStack> </VStack>
<div <ControlsButton
className={css({ i18nKeyPrefix={keyPrefix}
marginBottom: '80px', handle={handleTranscript}
width: '100%', statuses={statuses}
})} isPendingToStart={isPendingToStart}
> isPendingToStop={isPendingToStop}
{statuses.isStopping || isPendingToStop ? ( />
<HStack width={'100%'} height={'46px'} justify="center">
<Spinner size={30} />
<Text variant="body">{t('button.saving')}</Text>
</HStack>
) : (
<>
{statuses.isStarted || statuses.isStarting || room.isRecording ? (
<Button
variant="tertiary"
fullWidth
onPress={() => handleTranscript()}
isDisabled={statuses.isStopping || isPendingToStop || isLoading}
data-attr="stop-transcript"
>
{t('button.stop')}
</Button>
) : (
<Button
variant="tertiary"
fullWidth
onPress={() => handleTranscript()}
isDisabled={isPendingToStart || !isRoomConnected || isLoading}
data-attr="start-transcript"
>
{t('button.start')}
</Button>
)}
</>
)}
</div>
<Dialog <Dialog
isOpen={!!isErrorDialogOpen} isOpen={!!isErrorDialogOpen}
role="alertdialog" role="alertdialog"

View File

@@ -1,22 +0,0 @@
import { useSnapshot } from 'valtio'
import { RecordingStatus, recordingStore } from '@/stores/recording'
import { RecordingMode } from '@/features/recording'
export const useIsRecordingActive = (mode: RecordingMode) => {
const recordingSnap = useSnapshot(recordingStore)
switch (mode) {
case RecordingMode.Transcript:
return [
RecordingStatus.TRANSCRIPT_STARTED,
RecordingStatus.TRANSCRIPT_STARTING,
RecordingStatus.TRANSCRIPT_STOPPING,
].includes(recordingSnap.status)
case RecordingMode.ScreenRecording:
return [
RecordingStatus.SCREEN_RECORDING_STARTED,
RecordingStatus.SCREEN_RECORDING_STARTING,
RecordingStatus.SCREEN_RECORDING_STOPPING,
].includes(recordingSnap.status)
}
}

View File

@@ -0,0 +1,36 @@
import { RecordingMode } from '@/features/recording'
import { useRoomMetadata } from './useRoomMetadata'
import { useMemo } from 'react'
export interface RecordingStatuses {
isStarting: boolean
isStarted: boolean
isSaving: boolean
isActive: boolean
}
export const useRecordingStatuses = (
mode: RecordingMode
): RecordingStatuses => {
const metadata = useRoomMetadata()
return useMemo(() => {
if (metadata && metadata?.recording_mode === mode) {
return {
isStarting: metadata.recording_status === 'starting',
isStarted: metadata.recording_status === 'started',
isSaving: metadata.recording_status === 'saving',
isActive: ['starting', 'started', 'saving'].includes(
metadata.recording_status
),
}
}
return {
isStarting: false,
isStarted: false,
isSaving: false,
isActive: false,
}
}, [mode, metadata])
}

View File

@@ -0,0 +1,18 @@
import { useRoomInfo } from '@livekit/components-react'
import { useMemo } from 'react'
export const useRoomMetadata = () => {
const { metadata } = useRoomInfo()
return useMemo(() => {
if (metadata) {
try {
return JSON.parse(metadata)
} catch (error) {
console.error('Failed to parse room metadata:', error)
return undefined
}
} else {
return undefined
}
}, [metadata])
}

View File

@@ -1,9 +1,9 @@
// hooks // hooks
export { useIsRecordingModeEnabled } from './hooks/useIsRecordingModeEnabled' export { useIsRecordingModeEnabled } from './hooks/useIsRecordingModeEnabled'
export { useHasRecordingAccess } from './hooks/useHasRecordingAccess' export { useHasRecordingAccess } from './hooks/useHasRecordingAccess'
export { useIsRecordingActive } from './hooks/useIsRecordingActive'
export { useHasFeatureWithoutAdminRights } from './hooks/useHasFeatureWithoutAdminRights' export { useHasFeatureWithoutAdminRights } from './hooks/useHasFeatureWithoutAdminRights'
export { useHumanizeRecordingMaxDuration } from './hooks/useHumanizeRecordingMaxDuration' export { useHumanizeRecordingMaxDuration } from './hooks/useHumanizeRecordingMaxDuration'
export { useRecordingStatuses } from './hooks/useRecordingStatuses'
// api // api
export { useStartRecording } from './api/startRecording' export { useStartRecording } from './api/startRecording'

View File

@@ -323,7 +323,8 @@
"button": { "button": {
"start": "Meeting transkribieren starten", "start": "Meeting transkribieren starten",
"stop": "Meeting transkribieren beenden", "stop": "Meeting transkribieren beenden",
"saving": "Speichern…" "saving": "Speichern…",
"starting": "Wird gestartet…"
}, },
"linkMore": "Mehr erfahren", "linkMore": "Mehr erfahren",
"notAdminOrOwner": { "notAdminOrOwner": {
@@ -368,7 +369,8 @@
"button": { "button": {
"start": "Meeting-Aufzeichnung starten", "start": "Meeting-Aufzeichnung starten",
"stop": "Meeting-Aufzeichnung beenden", "stop": "Meeting-Aufzeichnung beenden",
"saving": "Wird gespeichert…" "saving": "Wird gespeichert…",
"starting": "Wird gestartet…"
}, },
"notAdminOrOwner": { "notAdminOrOwner": {
"heading": "Zugriff eingeschränkt", "heading": "Zugriff eingeschränkt",
@@ -507,7 +509,7 @@
"starting": "Transkription wird gestartet", "starting": "Transkription wird gestartet",
"stopping": "Transkription wird gestoppt" "stopping": "Transkription wird gestoppt"
}, },
"screenRecording": { "screen_recording": {
"started": "Aufzeichnung läuft", "started": "Aufzeichnung läuft",
"starting": "Aufzeichnung wird gestartet", "starting": "Aufzeichnung wird gestartet",
"stopping": "Aufzeichnung wird gestoppt" "stopping": "Aufzeichnung wird gestoppt"

View File

@@ -323,7 +323,8 @@
"button": { "button": {
"start": "Start transcribing the meeting", "start": "Start transcribing the meeting",
"stop": "Stop transcribing the meeting", "stop": "Stop transcribing the meeting",
"saving": "Saving…" "saving": "Saving…",
"starting": "Starting…"
}, },
"linkMore": "Learn more", "linkMore": "Learn more",
"notAdminOrOwner": { "notAdminOrOwner": {
@@ -368,7 +369,8 @@
"button": { "button": {
"start": "Start recording the meeting", "start": "Start recording the meeting",
"stop": "Stop recording the meeting", "stop": "Stop recording the meeting",
"saving": "Saving…" "saving": "Saving…",
"starting": "Starting…"
}, },
"notAdminOrOwner": { "notAdminOrOwner": {
"heading": "Restricted Access", "heading": "Restricted Access",
@@ -507,7 +509,7 @@
"starting": "Transcription starting", "starting": "Transcription starting",
"stopping": "Transcription stopping" "stopping": "Transcription stopping"
}, },
"screenRecording": { "screen_recording": {
"started": "Recording in progress", "started": "Recording in progress",
"starting": "Starting recording", "starting": "Starting recording",
"stopping": "Stopping recording" "stopping": "Stopping recording"

View File

@@ -323,7 +323,8 @@
"button": { "button": {
"start": "Commencer à transcrire la réunion", "start": "Commencer à transcrire la réunion",
"stop": "Arrêter de transcrire la réunion", "stop": "Arrêter de transcrire la réunion",
"saving": "Sauvegarde…" "saving": "Sauvegarde…",
"starting": "Démarrage…"
}, },
"linkMore": "En savoir plus", "linkMore": "En savoir plus",
"notAdminOrOwner": { "notAdminOrOwner": {
@@ -368,7 +369,8 @@
"button": { "button": {
"start": "Commencer à enregistrer la réunion", "start": "Commencer à enregistrer la réunion",
"stop": "Arrêter d'enregistrer la réunion", "stop": "Arrêter d'enregistrer la réunion",
"saving": "Sauvegarde…" "saving": "Sauvegarde…",
"starting": "Démarrage…"
}, },
"notAdminOrOwner": { "notAdminOrOwner": {
"heading": "Accès restreint", "heading": "Accès restreint",
@@ -507,7 +509,7 @@
"starting": "Démarrage de la transcription", "starting": "Démarrage de la transcription",
"stopping": "Arrêt de la transcription" "stopping": "Arrêt de la transcription"
}, },
"screenRecording": { "screen_recording": {
"started": "Enregistrement en cours", "started": "Enregistrement en cours",
"starting": "Démarrage de l'enregistrement", "starting": "Démarrage de l'enregistrement",
"stopping": "Arrêt de l'enregistrement" "stopping": "Arrêt de l'enregistrement"

View File

@@ -323,7 +323,8 @@
"button": { "button": {
"start": "Begin met het transcriberen van de vergadering", "start": "Begin met het transcriberen van de vergadering",
"stop": "Stop met het transcriberen van de vergadering", "stop": "Stop met het transcriberen van de vergadering",
"saving": "Opslaan…" "saving": "Opslaan…",
"starting": "Wordt gestart…"
}, },
"linkMore": "Meer informatie", "linkMore": "Meer informatie",
"notAdminOrOwner": { "notAdminOrOwner": {
@@ -368,7 +369,8 @@
"button": { "button": {
"start": "Start met opnemen van de vergadering", "start": "Start met opnemen van de vergadering",
"stop": "Stop met opnemen van de vergadering", "stop": "Stop met opnemen van de vergadering",
"saving": "Bezig met opslaan…" "saving": "Bezig met opslaan…",
"starting": "Wordt gestart…"
}, },
"notAdminOrOwner": { "notAdminOrOwner": {
"heading": "Toegang beperkt", "heading": "Toegang beperkt",
@@ -507,7 +509,7 @@
"starting": "Transcriptie begint", "starting": "Transcriptie begint",
"stopping": "Transcriptie stopt" "stopping": "Transcriptie stopt"
}, },
"screenRecording": { "screen_recording": {
"started": "Opname bezig", "started": "Opname bezig",
"starting": "Opname starten", "starting": "Opname starten",
"stopping": "Opname stoppen" "stopping": "Opname stoppen"

View File

@@ -6,23 +6,10 @@ export enum RecordingLanguage {
AUTOMATIC = 'auto', AUTOMATIC = 'auto',
} }
export enum RecordingStatus {
TRANSCRIPT_STARTING,
TRANSCRIPT_STARTED,
TRANSCRIPT_STOPPING,
STOPPED,
SCREEN_RECORDING_STARTING,
SCREEN_RECORDING_STARTED,
SCREEN_RECORDING_STOPPING,
ANY_STARTED,
}
type State = { type State = {
status: RecordingStatus
language: RecordingLanguage language: RecordingLanguage
} }
export const recordingStore = proxy<State>({ export const recordingStore = proxy<State>({
status: RecordingStatus.STOPPED,
language: RecordingLanguage.FRENCH, language: RecordingLanguage.FRENCH,
}) })