✨(frontend) handle another recording mode is active
Refactor literals in the recording status hook and introduce a new status. Align the login prompt style with the newly introduced warning message, and guide users by clearly indicating that the two modes are mutually exclusive. Users are prompted to stop the other mode before starting a new one. This situation should happen less often now that checkboxes allow users to start transcription and recording together. Hopefully, the UX is clear enough. The growing number of props passed to the controls buttons may become an issue and will likely require refactoring later.
This commit is contained in:
committed by
aleb_the_flash
parent
9d69fe4f4f
commit
70403ad0d8
@@ -1,4 +1,4 @@
|
||||
import { css } from '@/styled-system/css'
|
||||
import { css, cx } from '@/styled-system/css'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { Spinner } from '@/primitives/Spinner'
|
||||
import { Button, Text } from '@/primitives'
|
||||
@@ -7,6 +7,8 @@ import { RecordingStatuses } from '../hooks/useRecordingStatuses'
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
import { ConnectionState } from 'livekit-client'
|
||||
import { Button as RACButton } from 'react-aria-components'
|
||||
import { parseLineBreaks } from '@/utils/parseLineBreaks'
|
||||
|
||||
const Layout = ({ children }: { children: ReactNode }) => (
|
||||
<div
|
||||
@@ -25,6 +27,7 @@ interface ControlsButtonProps {
|
||||
handle: () => void
|
||||
isPendingToStart: boolean
|
||||
isPendingToStop: boolean
|
||||
openSidePanel: () => void
|
||||
}
|
||||
|
||||
const MIN_SPINNER_DISPLAY_TIME = 2000
|
||||
@@ -35,6 +38,7 @@ export const ControlsButton = ({
|
||||
handle,
|
||||
isPendingToStart,
|
||||
isPendingToStop,
|
||||
openSidePanel,
|
||||
}: ControlsButtonProps) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: i18nKeyPrefix })
|
||||
|
||||
@@ -45,7 +49,7 @@ export const ControlsButton = ({
|
||||
const timeoutRef = useRef<NodeJS.Timeout>()
|
||||
|
||||
const isSaving = statuses.isSaving || isPendingToStop
|
||||
const isDisabled = !isRoomConnected
|
||||
const isDisabled = !isRoomConnected || statuses.isAnotherModeStarted
|
||||
|
||||
useEffect(() => {
|
||||
if (isSaving) {
|
||||
@@ -103,8 +107,57 @@ export const ControlsButton = ({
|
||||
// Inactive state (Start button)
|
||||
return (
|
||||
<Layout>
|
||||
{statuses.isAnotherModeStarted && (
|
||||
<RACButton
|
||||
className={css({
|
||||
backgroundColor: 'primary.50',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.200',
|
||||
borderRadius: '6px',
|
||||
padding: '0.75rem',
|
||||
marginBottom: '0.75rem',
|
||||
display: 'flex',
|
||||
justifyContent: 'left',
|
||||
textAlign: 'left',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
cursor: 'pointer',
|
||||
_hover: {
|
||||
backgroundColor: 'primary.100',
|
||||
borderColor: 'primary.400',
|
||||
},
|
||||
})}
|
||||
onPress={() => openSidePanel()}
|
||||
>
|
||||
<span
|
||||
className={cx(
|
||||
'material-icons',
|
||||
css({
|
||||
color: 'primary.500',
|
||||
marginRight: '1rem',
|
||||
})
|
||||
)}
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<Text variant={'smNote'}>
|
||||
{parseLineBreaks(t('button.anotherModeStarted'))}
|
||||
</Text>
|
||||
<span
|
||||
className={cx(
|
||||
'material-icons',
|
||||
css({
|
||||
color: 'primary.500',
|
||||
marginLeft: 'auto',
|
||||
})
|
||||
)}
|
||||
>
|
||||
chevron_right
|
||||
</span>
|
||||
</RACButton>
|
||||
)}
|
||||
<Button
|
||||
variant="tertiary"
|
||||
variant={isDisabled ? 'primary' : 'tertiary'}
|
||||
fullWidth
|
||||
onPress={handle}
|
||||
isDisabled={isDisabled}
|
||||
|
||||
@@ -13,6 +13,8 @@ export const LoginPrompt = ({ heading, body }: LoginPromptProps) => {
|
||||
className={css({
|
||||
backgroundColor: 'primary.50',
|
||||
borderRadius: '5px',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.200',
|
||||
paddingY: '1rem',
|
||||
paddingX: '1rem',
|
||||
marginTop: '1rem',
|
||||
|
||||
@@ -27,6 +27,7 @@ import { VStack } from '@/styled-system/jsx'
|
||||
import { Checkbox } from '@/primitives/Checkbox'
|
||||
import { useTranscriptionLanguage } from '@/features/settings'
|
||||
import { useMutateRecording } from '../hooks/useMutateRecording'
|
||||
import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
|
||||
|
||||
export const ScreenRecordingSidePanel = () => {
|
||||
const { data } = useConfig()
|
||||
@@ -54,6 +55,7 @@ export const ScreenRecordingSidePanel = () => {
|
||||
const statuses = useRecordingStatuses(RecordingMode.ScreenRecording)
|
||||
|
||||
const room = useRoomContext()
|
||||
const { openTranscript } = useSidePanel()
|
||||
|
||||
const handleScreenRecording = async () => {
|
||||
if (!roomId) {
|
||||
@@ -184,6 +186,7 @@ export const ScreenRecordingSidePanel = () => {
|
||||
statuses={statuses}
|
||||
isPendingToStart={isPendingToStart}
|
||||
isPendingToStop={isPendingToStop}
|
||||
openSidePanel={openTranscript}
|
||||
/>
|
||||
</Div>
|
||||
)
|
||||
|
||||
@@ -32,6 +32,7 @@ import { NoAccessView } from './NoAccessView'
|
||||
import { ControlsButton } from './ControlsButton'
|
||||
import { RowWrapper } from './RowWrapper'
|
||||
import { useMutateRecording } from '../hooks/useMutateRecording'
|
||||
import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
|
||||
|
||||
export const TranscriptSidePanel = () => {
|
||||
const { data } = useConfig()
|
||||
@@ -66,6 +67,7 @@ export const TranscriptSidePanel = () => {
|
||||
const statuses = useRecordingStatuses(RecordingMode.Transcript)
|
||||
|
||||
const room = useRoomContext()
|
||||
const { openScreenRecording } = useSidePanel()
|
||||
|
||||
const handleTranscript = async () => {
|
||||
if (!roomId) {
|
||||
@@ -241,6 +243,7 @@ export const TranscriptSidePanel = () => {
|
||||
statuses={statuses}
|
||||
isPendingToStart={isPendingToStart}
|
||||
isPendingToStop={isPendingToStop}
|
||||
openSidePanel={openScreenRecording}
|
||||
/>
|
||||
</Div>
|
||||
)
|
||||
|
||||
@@ -2,7 +2,20 @@ import { RecordingMode } from '@/features/recording'
|
||||
import { useRoomMetadata } from './useRoomMetadata'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
export enum RecordingStatus {
|
||||
Starting = 'starting',
|
||||
Started = 'started',
|
||||
Saving = 'saving',
|
||||
}
|
||||
|
||||
const ACTIVE_STATUSES = [
|
||||
RecordingStatus.Starting,
|
||||
RecordingStatus.Started,
|
||||
RecordingStatus.Saving,
|
||||
] as const
|
||||
|
||||
export interface RecordingStatuses {
|
||||
isAnotherModeStarted: boolean
|
||||
isStarting: boolean
|
||||
isStarted: boolean
|
||||
isSaving: boolean
|
||||
@@ -17,16 +30,23 @@ export const useRecordingStatuses = (
|
||||
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
|
||||
isAnotherModeStarted: false,
|
||||
isStarting: metadata.recording_status === RecordingStatus.Starting,
|
||||
isStarted: metadata.recording_status === RecordingStatus.Started,
|
||||
isSaving: metadata.recording_status === RecordingStatus.Saving,
|
||||
isActive: ACTIVE_STATUSES.includes(
|
||||
metadata.recording_status as RecordingStatus
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const isAnotherModeStarted =
|
||||
!!metadata?.recording_mode &&
|
||||
metadata?.recording_mode !== mode &&
|
||||
ACTIVE_STATUSES.includes(metadata.recording_status as RecordingStatus)
|
||||
|
||||
return {
|
||||
isAnotherModeStarted,
|
||||
isStarting: false,
|
||||
isStarted: false,
|
||||
isSaving: false,
|
||||
|
||||
@@ -324,7 +324,8 @@
|
||||
"start": "Meeting transkribieren starten",
|
||||
"stop": "Meeting transkribieren beenden",
|
||||
"saving": "Speichern…",
|
||||
"starting": "Wird gestartet…"
|
||||
"starting": "Wird gestartet…",
|
||||
"anotherModeStarted": "Eine Aufnahme läuft. <br/> Wechseln Sie das Menü und stoppen Sie sie."
|
||||
},
|
||||
"linkMore": "Mehr erfahren",
|
||||
"notAdminOrOwner": {
|
||||
@@ -362,7 +363,8 @@
|
||||
"start": "Meeting-Aufzeichnung starten",
|
||||
"stop": "Meeting-Aufzeichnung beenden",
|
||||
"saving": "Wird gespeichert…",
|
||||
"starting": "Wird gestartet…"
|
||||
"starting": "Wird gestartet…",
|
||||
"anotherModeStarted": "Eine Transkription läuft. <br/> Wechseln Sie das Menü und stoppen Sie sie."
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Zugriff eingeschränkt",
|
||||
|
||||
@@ -324,7 +324,8 @@
|
||||
"start": "Start transcribing the meeting",
|
||||
"stop": "Stop transcribing the meeting",
|
||||
"saving": "Saving…",
|
||||
"starting": "Starting…"
|
||||
"starting": "Starting…",
|
||||
"anotherModeStarted": "Screen recording is running. Switch panels and stop it."
|
||||
},
|
||||
"linkMore": "Learn more",
|
||||
"notAdminOrOwner": {
|
||||
@@ -362,7 +363,8 @@
|
||||
"start": "Start recording the meeting",
|
||||
"stop": "Stop recording the meeting",
|
||||
"saving": "Saving…",
|
||||
"starting": "Starting…"
|
||||
"starting": "Starting…",
|
||||
"anotherModeStarted": "Transcription is running. <br/> Switch panels and stop it."
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Restricted Access",
|
||||
|
||||
@@ -324,7 +324,8 @@
|
||||
"start": "Commencer à transcrire la réunion",
|
||||
"stop": "Arrêter de transcrire la réunion",
|
||||
"saving": "Sauvegarde…",
|
||||
"starting": "Démarrage…"
|
||||
"starting": "Démarrage…",
|
||||
"anotherModeStarted": "Un enregistrement est en cours. <br/> Changez de menu et arrêtez le."
|
||||
},
|
||||
"linkMore": "En savoir plus",
|
||||
"notAdminOrOwner": {
|
||||
@@ -362,7 +363,8 @@
|
||||
"start": "Commencer à enregistrer la réunion",
|
||||
"stop": "Arrêter d'enregistrer la réunion",
|
||||
"saving": "Sauvegarde…",
|
||||
"starting": "Démarrage…"
|
||||
"starting": "Démarrage…",
|
||||
"anotherModeStarted": "Une transcription est en cours. <br/> Changez de menu et arrêtez la."
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Accès restreint",
|
||||
|
||||
@@ -324,7 +324,8 @@
|
||||
"start": "Begin met het transcriberen van de vergadering",
|
||||
"stop": "Stop met het transcriberen van de vergadering",
|
||||
"saving": "Opslaan…",
|
||||
"starting": "Wordt gestart…"
|
||||
"starting": "Wordt gestart…",
|
||||
"anotherModeStarted": "Er loopt een opname. <br/> Ga naar het menu en stop deze."
|
||||
},
|
||||
"linkMore": "Meer informatie",
|
||||
"notAdminOrOwner": {
|
||||
@@ -362,7 +363,8 @@
|
||||
"start": "Start met opnemen van de vergadering",
|
||||
"stop": "Stop met opnemen van de vergadering",
|
||||
"saving": "Bezig met opslaan…",
|
||||
"starting": "Wordt gestart…"
|
||||
"starting": "Wordt gestart…",
|
||||
"anotherModeStarted": "Er loopt een transcriptie. <br/> Ga naar het menu en stop deze."
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Toegang beperkt",
|
||||
|
||||
12
src/frontend/src/utils/parseLineBreaks.tsx
Normal file
12
src/frontend/src/utils/parseLineBreaks.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Fragment, ReactNode } from 'react'
|
||||
|
||||
export const parseLineBreaks = (text: string): ReactNode[] => {
|
||||
const parts = text.split(/(<br\s*\/?>)/gi)
|
||||
|
||||
return parts.map((part, index) => {
|
||||
if (part.match(/^<br\s*\/?>$/gi)) {
|
||||
return <br key={index} />
|
||||
}
|
||||
return <Fragment key={index}>{part}</Fragment>
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user