♻️(frontend) introduce a recording mutation hook

Mutualize and factorize the recording API error modal in a single place, and
extract all recording mutations into a dedicated hook exposing both start and
stop actions.

This hook is responsible for interacting with the API error dialog when needed.
Previously, this logic was duplicated across each side panel; centralizing it
clarifies responsibilities and reduces duplication.
This commit is contained in:
lebaudantoine
2026-01-01 01:35:55 +01:00
committed by aleb_the_flash
parent 08f281e778
commit 9d69fe4f4f
10 changed files with 101 additions and 122 deletions

View File

@@ -0,0 +1,29 @@
import { Button, Dialog, P } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { useSnapshot } from 'valtio'
import { recordingStore } from '@/stores/recording'
export const ErrorAlertDialog = () => {
const recordingSnap = useSnapshot(recordingStore)
const { t } = useTranslation('rooms', {
keyPrefix: 'errorRecordingAlertDialog',
})
return (
<Dialog
isOpen={!!recordingSnap.isErrorDialogOpen}
role="alertdialog"
title={t('title')}
aria-label={t('title')}
>
<P>{t(`body.${recordingSnap.isErrorDialogOpen}`)}</P>
<Button
variant="text"
size="sm"
onPress={() => (recordingStore.isErrorDialogOpen = '')}
>
{t('button')}
</Button>
</Dialog>
)
}

View File

@@ -1,11 +1,13 @@
import { LimitReachedAlertDialog } from './LimitReachedAlertDialog'
import { RecordingStateToast } from './RecordingStateToast'
import { ErrorAlertDialog } from './ErrorAlertDialog'
export const RecordingProvider = () => {
return (
<>
<RecordingStateToast />
<LimitReachedAlertDialog />
<ErrorAlertDialog />
</>
)
}

View File

@@ -1,4 +1,4 @@
import { A, Button, Dialog, Div, H, P, Text } from '@/primitives'
import { A, Div, H, Text } from '@/primitives'
import { css } from '@/styled-system/css'
import { useRoomId } from '@/features/rooms/livekit/hooks/useRoomId'
@@ -6,8 +6,6 @@ import { useRoomContext } from '@livekit/components-react'
import {
RecordingMode,
useHasFeatureWithoutAdminRights,
useStartRecording,
useStopRecording,
useHumanizeRecordingMaxDuration,
useRecordingStatuses,
} from '@/features/recording'
@@ -28,6 +26,7 @@ import { RowWrapper } from './RowWrapper'
import { VStack } from '@/styled-system/jsx'
import { Checkbox } from '@/primitives/Checkbox'
import { useTranscriptionLanguage } from '@/features/settings'
import { useMutateRecording } from '../hooks/useMutateRecording'
export const ScreenRecordingSidePanel = () => {
const { data } = useConfig()
@@ -36,8 +35,6 @@ export const ScreenRecordingSidePanel = () => {
const keyPrefix = 'screenRecording'
const { t } = useTranslation('rooms', { keyPrefix })
const [isErrorDialogOpen, setIsErrorDialogOpen] = useState('')
const [includeTranscript, setIncludeTranscript] = useState(false)
const hasFeatureWithoutAdminRights = useHasFeatureWithoutAdminRights(
@@ -51,14 +48,8 @@ export const ScreenRecordingSidePanel = () => {
const roomId = useRoomId()
const { mutateAsync: startRecordingRoom, isPending: isPendingToStart } =
useStartRecording({
onError: () => setIsErrorDialogOpen('start'),
})
const { mutateAsync: stopRecordingRoom, isPending: isPendingToStop } =
useStopRecording({
onError: () => setIsErrorDialogOpen('stop'),
})
const { startRecording, isPendingToStart, stopRecording, isPendingToStop } =
useMutateRecording()
const statuses = useRecordingStatuses(RecordingMode.ScreenRecording)
@@ -72,7 +63,7 @@ export const ScreenRecordingSidePanel = () => {
try {
if (statuses.isStarted || statuses.isStarting) {
setIncludeTranscript(false)
await stopRecordingRoom({ id: roomId })
await stopRecording({ id: roomId })
await notifyParticipants({
type: NotificationType.ScreenRecordingStopped,
@@ -89,7 +80,7 @@ export const ScreenRecordingSidePanel = () => {
...(includeTranscript && { transcribe: true }),
}
await startRecordingRoom({
await startRecording({
id: roomId,
mode: RecordingMode.ScreenRecording,
options: recordingOptions,
@@ -194,20 +185,6 @@ export const ScreenRecordingSidePanel = () => {
isPendingToStart={isPendingToStart}
isPendingToStop={isPendingToStop}
/>
<Dialog
isOpen={!!isErrorDialogOpen}
role="alertdialog"
aria-label={t('alert.title')}
>
<P>{t(`alert.body.${isErrorDialogOpen}`)}</P>
<Button
variant="text"
size="sm"
onPress={() => setIsErrorDialogOpen('')}
>
{t('alert.button')}
</Button>
</Dialog>
</Div>
)
}

View File

@@ -1,4 +1,4 @@
import { A, Button, Dialog, Div, H, P, Text } from '@/primitives'
import { A, Button, Div, H, Text } from '@/primitives'
import { css } from '@/styled-system/css'
import { useRoomId } from '@/features/rooms/livekit/hooks/useRoomId'
@@ -6,8 +6,6 @@ import { useRoomContext } from '@livekit/components-react'
import {
RecordingMode,
useHasRecordingAccess,
useStartRecording,
useStopRecording,
useHasFeatureWithoutAdminRights,
useHumanizeRecordingMaxDuration,
useRecordingStatuses,
@@ -33,6 +31,7 @@ import {
import { NoAccessView } from './NoAccessView'
import { ControlsButton } from './ControlsButton'
import { RowWrapper } from './RowWrapper'
import { useMutateRecording } from '../hooks/useMutateRecording'
export const TranscriptSidePanel = () => {
const { data } = useConfig()
@@ -41,7 +40,6 @@ export const TranscriptSidePanel = () => {
const keyPrefix = 'transcript'
const { t } = useTranslation('rooms', { keyPrefix })
const [isErrorDialogOpen, setIsErrorDialogOpen] = useState('')
const [includeScreenRecording, setIncludeScreenRecording] = useState(false)
const { notifyParticipants } = useNotifyParticipants()
@@ -62,15 +60,8 @@ export const TranscriptSidePanel = () => {
const roomId = useRoomId()
const { mutateAsync: startRecordingRoom, isPending: isPendingToStart } =
useStartRecording({
onError: () => setIsErrorDialogOpen('start'),
})
const { mutateAsync: stopRecordingRoom, isPending: isPendingToStop } =
useStopRecording({
onError: () => setIsErrorDialogOpen('stop'),
})
const { startRecording, isPendingToStart, stopRecording, isPendingToStop } =
useMutateRecording()
const statuses = useRecordingStatuses(RecordingMode.Transcript)
@@ -83,7 +74,7 @@ export const TranscriptSidePanel = () => {
}
try {
if (statuses.isStarted || statuses.isStarting) {
await stopRecordingRoom({ id: roomId })
await stopRecording({ id: roomId })
setIncludeScreenRecording(false)
await notifyParticipants({
@@ -108,7 +99,7 @@ export const TranscriptSidePanel = () => {
}),
}
await startRecordingRoom({
await startRecording({
id: roomId,
mode: recordingMode,
options: recordingOptions,
@@ -251,20 +242,6 @@ export const TranscriptSidePanel = () => {
isPendingToStart={isPendingToStart}
isPendingToStop={isPendingToStop}
/>
<Dialog
isOpen={!!isErrorDialogOpen}
role="alertdialog"
aria-label={t('alert.title')}
>
<P>{t(`alert.body.${isErrorDialogOpen}`)}</P>
<Button
variant="text"
size="sm"
onPress={() => setIsErrorDialogOpen('')}
>
{t('alert.button')}
</Button>
</Dialog>
</Div>
)
}

View File

@@ -0,0 +1,24 @@
import { useStartRecording, useStopRecording } from '@/features/recording'
import { recordingStore } from '@/stores/recording'
export const useMutateRecording = () => {
const { mutateAsync: startRecording, isPending: isPendingToStart } =
useStartRecording({
onError: () => {
recordingStore.isErrorDialogOpen = 'start'
},
})
const { mutateAsync: stopRecording, isPending: isPendingToStop } =
useStopRecording({
onError: () => {
recordingStore.isErrorDialogOpen = 'stop'
},
})
return {
startRecording,
isPendingToStart,
stopRecording,
isPendingToStop,
}
}

View File

@@ -344,14 +344,6 @@
"heading": "Anmeldung erforderlich",
"body": "Nur der Ersteller des Meetings oder ein Administrator kann die Transkription starten. Melden Sie sich an, um Ihre Berechtigungen zu überprüfen."
}
},
"alert": {
"title": "Transkription fehlgeschlagen",
"body": {
"stop": "Die Transkription konnte nicht gestoppt werden. Bitte versuche es in einem Moment erneut.",
"start": "Die Transkription konnte nicht gestartet werden. Bitte versuche es in einem Moment erneut."
},
"button": "OK"
}
},
"screenRecording": {
@@ -381,16 +373,16 @@
"body": "Nur der Ersteller der Besprechung oder ein Administrator kann die Aufzeichnung starten. Melden Sie sich an, um Ihre Berechtigungen zu überprüfen."
}
},
"alert": {
"title": "Aufzeichnung fehlgeschlagen",
"body": {
"stop": "Die Aufzeichnung konnte nicht gestoppt werden. Bitte versuche es in einem Moment erneut.",
"start": "Die Aufzeichnung konnte nicht gestartet werden. Bitte versuche es in einem Moment erneut."
},
"button": "OK"
},
"durationMessage": "(begrenzt auf {{max_duration}})"
},
"errorRecordingAlertDialog": {
"title": "Aufzeichnung fehlgeschlagen",
"body": {
"stop": "Die Aufzeichnung konnte nicht gestoppt werden. Bitte versuche es in einem Moment erneut.",
"start": "Die Aufzeichnung konnte nicht gestartet werden. Bitte versuche es in einem Moment erneut."
},
"button": "OK"
},
"admin": {
"description": "Diese Einstellungen für Organisatoren ermöglichen dir die Kontrolle über dein Meeting. Nur Organisatoren haben Zugriff auf diese Optionen.",
"access": {

View File

@@ -344,14 +344,6 @@
"heading": "You are not logged in!",
"body": "You must be logged in to use this feature. Please log in, then try again."
}
},
"alert": {
"title": "Transcription Failed",
"body": {
"stop": "We were unable to stop the transcription. Please try again in a moment.",
"start": "We were unable to start the transcription. Please try again in a moment."
},
"button": "OK"
}
},
"screenRecording": {
@@ -381,16 +373,16 @@
"body": "Only the meeting creator or an admin can start screen recording. Log in to verify your permissions."
}
},
"alert": {
"title": "Recording Failed",
"body": {
"stop": "We were unable to stop the recording. Please try again in a moment.",
"start": "We were unable to start the recording. Please try again in a moment."
},
"button": "OK"
},
"durationMessage": "(limited to {{max_duration}}) "
},
"errorRecordingAlertDialog": {
"title": "Recording Failed",
"body": {
"stop": "We were unable to stop the recording. Please try again in a moment.",
"start": "We were unable to start the recording. Please try again in a moment."
},
"button": "OK"
},
"admin": {
"description": "These organizer settings allow you to maintain control of your meeting. Only organizers can access these controls.",
"access": {

View File

@@ -344,14 +344,6 @@
"heading": "Vous n'êtes pas connecté !",
"body": "Vous devez être connecté pour utiliser cette fonctionnalité. Connectez-vous, puis réessayez."
}
},
"alert": {
"title": "Échec de transcription",
"body": {
"stop": "Nous n'avons pas pu stopper la transcription. Veuillez réessayer dans quelques instants.",
"start": "Nous n'avons pas pu démarrer la transcription. Veuillez réessayer dans quelques instants."
},
"button": "OK"
}
},
"screenRecording": {
@@ -381,16 +373,16 @@
"body": "Seul le créateur de la réunion ou un administrateur peut démarrer l'enregistrement. Connectez-vous pour vérifier vos autorisations."
}
},
"alert": {
"title": "Échec de l'enregistrement",
"body": {
"stop": "Nous n'avons pas pu stopper l'enregistrement. Veuillez réessayer dans quelques instants.",
"start": "Nous n'avons pas pu démarrer l'enregistrement. Veuillez réessayer dans quelques instants."
},
"button": "OK"
},
"durationMessage": "(limité à {{max_duration}}) "
},
"errorRecordingAlertDialog": {
"title": "Échec de l'enregistrement",
"body": {
"stop": "Nous n'avons pas pu stopper l'enregistrement. Veuillez réessayer dans quelques instants.",
"start": "Nous n'avons pas pu démarrer l'enregistrement. Veuillez réessayer dans quelques instants."
},
"button": "OK"
},
"admin": {
"description": "Ces paramètres organisateur vous permettent de garder le contrôle de votre réunion. Seuls les organisateurs peuvent accéder à ces commandes.",
"access": {

View File

@@ -344,14 +344,6 @@
"heading": "Inloggen vereist",
"body": "Alleen de maker van de vergadering of een beheerder kan de transcriptie starten. Log in om uw machtigingen te controleren."
}
},
"alert": {
"title": "Transcriptie mislukt",
"body": {
"stop": "We konden de transcriptie niet stoppen. Probeer het over enkele ogenblikken opnieuw.",
"start": "We konden de transcriptie niet starten. Probeer het over enkele ogenblikken opnieuw."
},
"button": "Opnieuw proberen"
}
},
"screenRecording": {
@@ -381,16 +373,16 @@
"body": "Alleen de maker van de vergadering of een beheerder kan de opname starten. Log in om uw machtigingen te controleren."
}
},
"alert": {
"title": "Opname mislukt",
"body": {
"stop": "We konden de opname niet stoppen. Probeer het over enkele ogenblikken opnieuw.",
"start": "We konden de opname niet starten. Probeer het over enkele ogenblikken opnieuw."
},
"button": "Opnieuw proberen"
},
"durationMessage": "(beperkt tot {{max_duration}})"
},
"errorRecordingAlertDialog": {
"title": "Opname mislukt",
"body": {
"stop": "We konden de opname niet stoppen. Probeer het over enkele ogenblikken opnieuw.",
"start": "We konden de opname niet starten. Probeer het over enkele ogenblikken opnieuw."
},
"button": "Opnieuw proberen"
},
"admin": {
"description": "Deze organisatorinstellingen geven u controle over uw vergadering. Alleen organisatoren hebben toegang tot deze bedieningselementen.",
"access": {

View File

@@ -8,8 +8,10 @@ export enum RecordingLanguage {
type State = {
language: RecordingLanguage
isErrorDialogOpen: string
}
export const recordingStore = proxy<State>({
language: RecordingLanguage.FRENCH,
isErrorDialogOpen: '',
})