✨(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:
committed by
aleb_the_flash
parent
16badde82d
commit
da3dfedcbc
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<NoAccessView
|
||||
i18nKeyPrefix="screenRecording"
|
||||
i18nKeyPrefix={keyPrefix}
|
||||
i18nKey="notAdminOrOwner"
|
||||
helpArticle={data?.support?.help_article_recording}
|
||||
imagePath="/assets/intro-slider/4.png"
|
||||
@@ -208,51 +181,19 @@ export const ScreenRecordingSidePanel = () => {
|
||||
size="sm"
|
||||
isSelected={includeTranscript}
|
||||
onChange={setIncludeTranscript}
|
||||
isDisabled={
|
||||
statuses.isStarting || statuses.isStarted || isPendingToStart
|
||||
}
|
||||
isDisabled={statuses.isActive || isPendingToStart}
|
||||
>
|
||||
<Text variant="sm">{t('details.transcription')}</Text>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</VStack>
|
||||
<div
|
||||
className={css({
|
||||
marginBottom: '80px',
|
||||
width: '100%',
|
||||
})}
|
||||
>
|
||||
{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>
|
||||
<ControlsButton
|
||||
i18nKeyPrefix={keyPrefix}
|
||||
handle={handleScreenRecording}
|
||||
statuses={statuses}
|
||||
isPendingToStart={isPendingToStart}
|
||||
isPendingToStop={isPendingToStop}
|
||||
/>
|
||||
<Dialog
|
||||
isOpen={!!isErrorDialogOpen}
|
||||
role="alertdialog"
|
||||
|
||||
@@ -10,11 +10,10 @@ import {
|
||||
useStopRecording,
|
||||
useHasFeatureWithoutAdminRights,
|
||||
useHumanizeRecordingMaxDuration,
|
||||
useRecordingStatuses,
|
||||
} from '../index'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { ConnectionState, RoomEvent } from 'livekit-client'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RecordingStatus } from '@store/recording'
|
||||
import { FeatureFlags } from '@/features/analytics/enums'
|
||||
import {
|
||||
NotificationType,
|
||||
@@ -22,8 +21,6 @@ import {
|
||||
notifyRecordingSaveInProgress,
|
||||
} from '@/features/notifications'
|
||||
import posthog from 'posthog-js'
|
||||
import { useSnapshot } from 'valtio/index'
|
||||
import { Spinner } from '@/primitives/Spinner'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { VStack } from '@/styled-system/jsx'
|
||||
import { Checkbox } from '@/primitives/Checkbox.tsx'
|
||||
@@ -34,20 +31,19 @@ import {
|
||||
useTranscriptionLanguage,
|
||||
} from '@/features/settings'
|
||||
import { NoAccessView } from './NoAccessView'
|
||||
import { ControlsButton } from './ControlsButton'
|
||||
import { RowWrapper } from './RowWrapper'
|
||||
|
||||
export const TranscriptSidePanel = () => {
|
||||
const { data } = useConfig()
|
||||
const recordingMaxDuration = useHumanizeRecordingMaxDuration()
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'transcript' })
|
||||
const keyPrefix = 'transcript'
|
||||
const { t } = useTranslation('rooms', { keyPrefix })
|
||||
|
||||
const [isErrorDialogOpen, setIsErrorDialogOpen] = useState('')
|
||||
const [includeScreenRecording, setIncludeScreenRecording] = useState(false)
|
||||
|
||||
const recordingSnap = useSnapshot(recordingStore)
|
||||
|
||||
const { notifyParticipants } = useNotifyParticipants()
|
||||
const { selectedLanguageKey, selectedLanguageLabel, isLanguageSetToAuto } =
|
||||
useTranscriptionLanguage()
|
||||
@@ -76,28 +72,9 @@ export const TranscriptSidePanel = () => {
|
||||
onError: () => setIsErrorDialogOpen('stop'),
|
||||
})
|
||||
|
||||
const statuses = useMemo(() => {
|
||||
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 statuses = useRecordingStatuses(RecordingMode.Transcript)
|
||||
|
||||
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 () => {
|
||||
if (!roomId) {
|
||||
@@ -105,11 +82,10 @@ export const TranscriptSidePanel = () => {
|
||||
return
|
||||
}
|
||||
try {
|
||||
setIsLoading(true)
|
||||
if (room.isRecording) {
|
||||
if (statuses.isStarted || statuses.isStarting) {
|
||||
await stopRecordingRoom({ id: roomId })
|
||||
setIncludeScreenRecording(false)
|
||||
recordingStore.status = RecordingStatus.TRANSCRIPT_STOPPING
|
||||
|
||||
await notifyParticipants({
|
||||
type: NotificationType.TranscriptionStopped,
|
||||
})
|
||||
@@ -126,8 +102,10 @@ export const TranscriptSidePanel = () => {
|
||||
...(!isLanguageSetToAuto && {
|
||||
language: selectedLanguageKey,
|
||||
}),
|
||||
...(includeScreenRecording && {
|
||||
transcribe: true,
|
||||
original_mode: RecordingMode.Transcript,
|
||||
}),
|
||||
...(includeScreenRecording && { transcribe: true }),
|
||||
}
|
||||
|
||||
await startRecordingRoom({
|
||||
@@ -135,7 +113,7 @@ export const TranscriptSidePanel = () => {
|
||||
mode: recordingMode,
|
||||
options: recordingOptions,
|
||||
})
|
||||
recordingStore.status = RecordingStatus.TRANSCRIPT_STARTING
|
||||
|
||||
await notifyParticipants({
|
||||
type: NotificationType.TranscriptionStarted,
|
||||
})
|
||||
@@ -143,14 +121,13 @@ export const TranscriptSidePanel = () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to handle transcript:', error)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasFeatureWithoutAdminRights) {
|
||||
return (
|
||||
<NoAccessView
|
||||
i18nKeyPrefix="transcript"
|
||||
i18nKeyPrefix={keyPrefix}
|
||||
i18nKey="notAdminOrOwner"
|
||||
helpArticle={data?.support?.help_article_transcript}
|
||||
imagePath="/assets/intro-slider/3.png"
|
||||
@@ -161,7 +138,7 @@ export const TranscriptSidePanel = () => {
|
||||
if (!hasTranscriptAccess) {
|
||||
return (
|
||||
<NoAccessView
|
||||
i18nKeyPrefix="transcript"
|
||||
i18nKeyPrefix={keyPrefix}
|
||||
i18nKey="premium"
|
||||
helpArticle={data?.support?.help_article_transcript}
|
||||
imagePath="/assets/intro-slider/3.png"
|
||||
@@ -251,7 +228,6 @@ export const TranscriptSidePanel = () => {
|
||||
</Text>
|
||||
</RowWrapper>
|
||||
<div className={css({ height: '15px' })} />
|
||||
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
@@ -262,51 +238,19 @@ export const TranscriptSidePanel = () => {
|
||||
size="sm"
|
||||
isSelected={includeScreenRecording}
|
||||
onChange={setIncludeScreenRecording}
|
||||
isDisabled={
|
||||
statuses.isStarting || statuses.isStarted || isPendingToStart
|
||||
}
|
||||
isDisabled={statuses.isActive || isPendingToStart}
|
||||
>
|
||||
<Text variant="sm">{t('details.recording')}</Text>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</VStack>
|
||||
<div
|
||||
className={css({
|
||||
marginBottom: '80px',
|
||||
width: '100%',
|
||||
})}
|
||||
>
|
||||
{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>
|
||||
<ControlsButton
|
||||
i18nKeyPrefix={keyPrefix}
|
||||
handle={handleTranscript}
|
||||
statuses={statuses}
|
||||
isPendingToStart={isPendingToStart}
|
||||
isPendingToStop={isPendingToStop}
|
||||
/>
|
||||
<Dialog
|
||||
isOpen={!!isErrorDialogOpen}
|
||||
role="alertdialog"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
18
src/frontend/src/features/recording/hooks/useRoomMetadata.ts
Normal file
18
src/frontend/src/features/recording/hooks/useRoomMetadata.ts
Normal 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])
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
// hooks
|
||||
export { useIsRecordingModeEnabled } from './hooks/useIsRecordingModeEnabled'
|
||||
export { useHasRecordingAccess } from './hooks/useHasRecordingAccess'
|
||||
export { useIsRecordingActive } from './hooks/useIsRecordingActive'
|
||||
export { useHasFeatureWithoutAdminRights } from './hooks/useHasFeatureWithoutAdminRights'
|
||||
export { useHumanizeRecordingMaxDuration } from './hooks/useHumanizeRecordingMaxDuration'
|
||||
export { useRecordingStatuses } from './hooks/useRecordingStatuses'
|
||||
|
||||
// api
|
||||
export { useStartRecording } from './api/startRecording'
|
||||
|
||||
@@ -323,7 +323,8 @@
|
||||
"button": {
|
||||
"start": "Meeting transkribieren starten",
|
||||
"stop": "Meeting transkribieren beenden",
|
||||
"saving": "Speichern…"
|
||||
"saving": "Speichern…",
|
||||
"starting": "Wird gestartet…"
|
||||
},
|
||||
"linkMore": "Mehr erfahren",
|
||||
"notAdminOrOwner": {
|
||||
@@ -368,7 +369,8 @@
|
||||
"button": {
|
||||
"start": "Meeting-Aufzeichnung starten",
|
||||
"stop": "Meeting-Aufzeichnung beenden",
|
||||
"saving": "Wird gespeichert…"
|
||||
"saving": "Wird gespeichert…",
|
||||
"starting": "Wird gestartet…"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Zugriff eingeschränkt",
|
||||
@@ -507,7 +509,7 @@
|
||||
"starting": "Transkription wird gestartet",
|
||||
"stopping": "Transkription wird gestoppt"
|
||||
},
|
||||
"screenRecording": {
|
||||
"screen_recording": {
|
||||
"started": "Aufzeichnung läuft",
|
||||
"starting": "Aufzeichnung wird gestartet",
|
||||
"stopping": "Aufzeichnung wird gestoppt"
|
||||
|
||||
@@ -323,7 +323,8 @@
|
||||
"button": {
|
||||
"start": "Start transcribing the meeting",
|
||||
"stop": "Stop transcribing the meeting",
|
||||
"saving": "Saving…"
|
||||
"saving": "Saving…",
|
||||
"starting": "Starting…"
|
||||
},
|
||||
"linkMore": "Learn more",
|
||||
"notAdminOrOwner": {
|
||||
@@ -368,7 +369,8 @@
|
||||
"button": {
|
||||
"start": "Start recording the meeting",
|
||||
"stop": "Stop recording the meeting",
|
||||
"saving": "Saving…"
|
||||
"saving": "Saving…",
|
||||
"starting": "Starting…"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Restricted Access",
|
||||
@@ -507,7 +509,7 @@
|
||||
"starting": "Transcription starting",
|
||||
"stopping": "Transcription stopping"
|
||||
},
|
||||
"screenRecording": {
|
||||
"screen_recording": {
|
||||
"started": "Recording in progress",
|
||||
"starting": "Starting recording",
|
||||
"stopping": "Stopping recording"
|
||||
|
||||
@@ -323,7 +323,8 @@
|
||||
"button": {
|
||||
"start": "Commencer à transcrire la réunion",
|
||||
"stop": "Arrêter de transcrire la réunion",
|
||||
"saving": "Sauvegarde…"
|
||||
"saving": "Sauvegarde…",
|
||||
"starting": "Démarrage…"
|
||||
},
|
||||
"linkMore": "En savoir plus",
|
||||
"notAdminOrOwner": {
|
||||
@@ -368,7 +369,8 @@
|
||||
"button": {
|
||||
"start": "Commencer à enregistrer la réunion",
|
||||
"stop": "Arrêter d'enregistrer la réunion",
|
||||
"saving": "Sauvegarde…"
|
||||
"saving": "Sauvegarde…",
|
||||
"starting": "Démarrage…"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Accès restreint",
|
||||
@@ -507,7 +509,7 @@
|
||||
"starting": "Démarrage de la transcription",
|
||||
"stopping": "Arrêt de la transcription"
|
||||
},
|
||||
"screenRecording": {
|
||||
"screen_recording": {
|
||||
"started": "Enregistrement en cours",
|
||||
"starting": "Démarrage de l'enregistrement",
|
||||
"stopping": "Arrêt de l'enregistrement"
|
||||
|
||||
@@ -323,7 +323,8 @@
|
||||
"button": {
|
||||
"start": "Begin met het transcriberen van de vergadering",
|
||||
"stop": "Stop met het transcriberen van de vergadering",
|
||||
"saving": "Opslaan…"
|
||||
"saving": "Opslaan…",
|
||||
"starting": "Wordt gestart…"
|
||||
},
|
||||
"linkMore": "Meer informatie",
|
||||
"notAdminOrOwner": {
|
||||
@@ -368,7 +369,8 @@
|
||||
"button": {
|
||||
"start": "Start met opnemen van de vergadering",
|
||||
"stop": "Stop met opnemen van de vergadering",
|
||||
"saving": "Bezig met opslaan…"
|
||||
"saving": "Bezig met opslaan…",
|
||||
"starting": "Wordt gestart…"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Toegang beperkt",
|
||||
@@ -507,7 +509,7 @@
|
||||
"starting": "Transcriptie begint",
|
||||
"stopping": "Transcriptie stopt"
|
||||
},
|
||||
"screenRecording": {
|
||||
"screen_recording": {
|
||||
"started": "Opname bezig",
|
||||
"starting": "Opname starten",
|
||||
"stopping": "Opname stoppen"
|
||||
|
||||
@@ -6,23 +6,10 @@ export enum RecordingLanguage {
|
||||
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 = {
|
||||
status: RecordingStatus
|
||||
language: RecordingLanguage
|
||||
}
|
||||
|
||||
export const recordingStore = proxy<State>({
|
||||
status: RecordingStatus.STOPPED,
|
||||
language: RecordingLanguage.FRENCH,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user