🚸(frontend) align screen recording side panel ux
Refactor the screen recording side panel to align with the transcription UX, ensuring a more consistent and homogeneous user experience. This commit also introduces a checkbox allowing users to request transcription of the screen recording, which is one of the most requested features. The side panel will be enriched with more information soon, especially once Fichier is integrated for storing recordings, so the destination can be made explicit. More recording settings (layout, quality, etc.) will be introduced in upcoming commits.
This commit is contained in:
committed by
aleb_the_flash
parent
236245740f
commit
5e1705d259
@@ -6,14 +6,17 @@ import { useRoomContext } from '@livekit/components-react'
|
||||
import {
|
||||
RecordingMode,
|
||||
useHasFeatureWithoutAdminRights,
|
||||
useIsRecordingTransitioning,
|
||||
useStartRecording,
|
||||
useStopRecording,
|
||||
} from '@/features/recording'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { ConnectionState, RoomEvent } from 'livekit-client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RecordingStatus, recordingStore } from '@/stores/recording'
|
||||
import {
|
||||
RecordingLanguage,
|
||||
RecordingStatus,
|
||||
recordingStore,
|
||||
} from '@/stores/recording'
|
||||
|
||||
import {
|
||||
NotificationType,
|
||||
@@ -28,6 +31,8 @@ import humanizeDuration from 'humanize-duration'
|
||||
import i18n from 'i18next'
|
||||
import { FeatureFlags } from '@/features/analytics/enums'
|
||||
import { NoAccessView } from './NoAccessView'
|
||||
import { HStack, VStack } from '@/styled-system/jsx'
|
||||
import { Checkbox } from '@/primitives/Checkbox'
|
||||
|
||||
export const ScreenRecordingSidePanel = () => {
|
||||
const { data } = useConfig()
|
||||
@@ -37,6 +42,8 @@ export const ScreenRecordingSidePanel = () => {
|
||||
|
||||
const [isErrorDialogOpen, setIsErrorDialogOpen] = useState('')
|
||||
|
||||
const [includeTranscript, setIncludeTranscript] = useState(false)
|
||||
|
||||
const hasFeatureWithoutAdminRights = useHasFeatureWithoutAdminRights(
|
||||
RecordingMode.ScreenRecording,
|
||||
FeatureFlags.ScreenRecording
|
||||
@@ -70,7 +77,6 @@ export const ScreenRecordingSidePanel = () => {
|
||||
|
||||
const room = useRoomContext()
|
||||
const isRoomConnected = room.state == ConnectionState.Connected
|
||||
const isRecordingTransitioning = useIsRecordingTransitioning()
|
||||
|
||||
useEffect(() => {
|
||||
const handleRecordingStatusChanged = () => {
|
||||
@@ -90,6 +96,7 @@ export const ScreenRecordingSidePanel = () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
if (room.isRecording) {
|
||||
setIncludeTranscript(false)
|
||||
await stopRecordingRoom({ id: roomId })
|
||||
recordingStore.status = RecordingStatus.SCREEN_RECORDING_STOPPING
|
||||
await notifyParticipants({
|
||||
@@ -100,9 +107,17 @@ export const ScreenRecordingSidePanel = () => {
|
||||
room.localParticipant
|
||||
)
|
||||
} else {
|
||||
const recordingOptions = {
|
||||
...(recordingSnap.language != RecordingLanguage.AUTOMATIC && {
|
||||
language: recordingSnap.language,
|
||||
}),
|
||||
...(includeTranscript && { transcribe: true }),
|
||||
}
|
||||
|
||||
await startRecordingRoom({
|
||||
id: roomId,
|
||||
mode: RecordingMode.ScreenRecording,
|
||||
options: recordingOptions,
|
||||
})
|
||||
recordingStore.status = RecordingStatus.SCREEN_RECORDING_STARTING
|
||||
await notifyParticipants({
|
||||
@@ -116,15 +131,6 @@ export const ScreenRecordingSidePanel = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const isDisabled = useMemo(
|
||||
() =>
|
||||
isLoading ||
|
||||
isRecordingTransitioning ||
|
||||
statuses.isAnotherModeStarted ||
|
||||
!isRoomConnected,
|
||||
[isLoading, isRecordingTransitioning, statuses, isRoomConnected]
|
||||
)
|
||||
|
||||
if (hasFeatureWithoutAdminRights) {
|
||||
return (
|
||||
<NoAccessView
|
||||
@@ -147,138 +153,158 @@ export const ScreenRecordingSidePanel = () => {
|
||||
>
|
||||
<img
|
||||
src="/assets/intro-slider/4.png"
|
||||
alt={''}
|
||||
alt=""
|
||||
className={css({
|
||||
minHeight: '309px',
|
||||
height: '309px',
|
||||
minHeight: '250px',
|
||||
height: '250px',
|
||||
marginBottom: '1rem',
|
||||
'@media (max-height: 700px)': {
|
||||
marginTop: '-16px',
|
||||
'@media (max-height: 900px)': {
|
||||
height: 'auto',
|
||||
minHeight: 'auto',
|
||||
maxHeight: '45%',
|
||||
marginBottom: '0.3rem',
|
||||
maxHeight: '25%',
|
||||
marginBottom: '0.75rem',
|
||||
},
|
||||
'@media (max-height: 530px)': {
|
||||
height: 'auto',
|
||||
minHeight: 'auto',
|
||||
maxHeight: '40%',
|
||||
marginBottom: '0.1rem',
|
||||
'@media (max-height: 770px)': {
|
||||
display: 'none',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
{statuses.isStarted ? (
|
||||
<>
|
||||
<H lvl={3} margin={false}>
|
||||
{t('stop.heading')}
|
||||
</H>
|
||||
<Text
|
||||
variant="note"
|
||||
wrap={'pretty'}
|
||||
centered
|
||||
<VStack gap={0} marginBottom={30}>
|
||||
<H lvl={1} margin={'sm'} fullWidth>
|
||||
{t('heading')}
|
||||
</H>
|
||||
<Text variant="body" fullWidth>
|
||||
{data?.recording?.max_duration
|
||||
? t('body', {
|
||||
max_duration: humanizeDuration(data?.recording?.max_duration, {
|
||||
language: i18n.language,
|
||||
}),
|
||||
})
|
||||
: t('bodyWithoutMaxDuration')}{' '}
|
||||
{data?.support?.help_article_recording && (
|
||||
<A href={data.support.help_article_recording} target="_blank">
|
||||
{t('linkMore')}
|
||||
</A>
|
||||
)}
|
||||
</Text>
|
||||
</VStack>
|
||||
<VStack gap={0} marginBottom={40}>
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
background: 'gray.100',
|
||||
borderRadius: '4px 4px 0 0',
|
||||
paddingLeft: '4px',
|
||||
padding: '8px',
|
||||
display: 'flex',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
textStyle: 'sm',
|
||||
marginBottom: '2.5rem',
|
||||
marginTop: '0.25rem',
|
||||
'@media (max-height: 700px)': {
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
})}
|
||||
>
|
||||
{t('stop.body')}
|
||||
</Text>
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
onPress={() => handleScreenRecording()}
|
||||
data-attr="stop-screen-recording"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
<span className="material-icons">cloud_download</span>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
flex: 5,
|
||||
})}
|
||||
>
|
||||
{t('stop.button')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{statuses.isStopping || isPendingToStop ? (
|
||||
<>
|
||||
<H lvl={3} margin={false}>
|
||||
{t('stopping.heading')}
|
||||
</H>
|
||||
<Text
|
||||
variant="note"
|
||||
wrap={'pretty'}
|
||||
centered
|
||||
className={css({
|
||||
textStyle: 'sm',
|
||||
maxWidth: '90%',
|
||||
marginBottom: '2.5rem',
|
||||
marginTop: '0.25rem',
|
||||
'@media (max-height: 700px)': {
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{t('stopping.body')}
|
||||
</Text>
|
||||
<Spinner />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<H lvl={3} margin={false}>
|
||||
{t('start.heading')}
|
||||
</H>
|
||||
<Text
|
||||
variant="note"
|
||||
wrap="balance"
|
||||
centered
|
||||
className={css({
|
||||
textStyle: 'sm',
|
||||
maxWidth: '90%',
|
||||
marginBottom: '2.5rem',
|
||||
marginTop: '0.25rem',
|
||||
'@media (max-height: 700px)': {
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{t('start.body', {
|
||||
duration_message: data?.recording?.max_duration
|
||||
? t('durationMessage', {
|
||||
max_duration: humanizeDuration(
|
||||
data?.recording?.max_duration,
|
||||
{
|
||||
language: i18n.language,
|
||||
}
|
||||
),
|
||||
})
|
||||
: '',
|
||||
})}{' '}
|
||||
{data?.support?.help_article_recording && (
|
||||
<A href={data.support.help_article_recording} target="_blank">
|
||||
{t('start.linkMore')}
|
||||
</A>
|
||||
)}
|
||||
</Text>
|
||||
<Text variant="sm">{t('details.destination')}</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
background: 'gray.100',
|
||||
borderRadius: '0 0 4px 4px',
|
||||
paddingLeft: '4px',
|
||||
padding: '8px',
|
||||
display: 'flex',
|
||||
marginTop: '4px',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
})}
|
||||
>
|
||||
<span className="material-icons">mail</span>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
flex: 5,
|
||||
})}
|
||||
>
|
||||
<Text variant="sm">{t('details.receiver')}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={css({ height: '15px' })} />
|
||||
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
marginLeft: '20px',
|
||||
})}
|
||||
>
|
||||
<Checkbox
|
||||
size="sm"
|
||||
isSelected={includeTranscript}
|
||||
onChange={setIncludeTranscript}
|
||||
isDisabled={
|
||||
statuses.isStarting || statuses.isStarted || 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
|
||||
isDisabled={isDisabled}
|
||||
onPress={() => handleScreenRecording()}
|
||||
data-attr="start-screen-recording"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
fullWidth
|
||||
onPress={() => handleScreenRecording()}
|
||||
isDisabled={statuses.isStopping || isPendingToStop || isLoading}
|
||||
data-attr="stop-transcript"
|
||||
>
|
||||
{statuses.isStarting || isPendingToStart ? (
|
||||
<>
|
||||
<Spinner size={20} />
|
||||
{t('start.loading')}
|
||||
</>
|
||||
) : (
|
||||
t('start.button')
|
||||
)}
|
||||
{t('button.stop')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
) : (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
fullWidth
|
||||
onPress={() => handleScreenRecording()}
|
||||
isDisabled={isPendingToStart || !isRoomConnected || isLoading}
|
||||
data-attr="start-transcript"
|
||||
>
|
||||
{t('button.start')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Dialog
|
||||
isOpen={!!isErrorDialogOpen}
|
||||
role="alertdialog"
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { RecordingStatus, recordingStore } from '@/stores/recording'
|
||||
|
||||
export const useIsRecordingTransitioning = () => {
|
||||
const recordingSnap = useSnapshot(recordingStore)
|
||||
|
||||
const transitionalStates = [
|
||||
RecordingStatus.TRANSCRIPT_STARTING,
|
||||
RecordingStatus.TRANSCRIPT_STOPPING,
|
||||
RecordingStatus.SCREEN_RECORDING_STARTING,
|
||||
RecordingStatus.SCREEN_RECORDING_STOPPING,
|
||||
]
|
||||
|
||||
return transitionalStates.includes(recordingSnap.status)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// hooks
|
||||
export { useIsRecordingModeEnabled } from './hooks/useIsRecordingModeEnabled'
|
||||
export { useIsRecordingTransitioning } from './hooks/useIsRecordingTransitioning'
|
||||
export { useHasRecordingAccess } from './hooks/useHasRecordingAccess'
|
||||
export { useIsRecordingActive } from './hooks/useIsRecordingActive'
|
||||
export { useHasFeatureWithoutAdminRights } from './hooks/useHasFeatureWithoutAdminRights'
|
||||
|
||||
@@ -354,12 +354,21 @@
|
||||
}
|
||||
},
|
||||
"screenRecording": {
|
||||
"start": {
|
||||
"heading": "Dieses Gespräch aufzeichnen",
|
||||
"body": "Zeichne dieses Gespräch auf, um es später anzusehen {{duration_message}}. Du erhältst die Videoaufnahme per E-Mail.",
|
||||
"button": "Aufzeichnung starten",
|
||||
"heading": "Diesen Anruf für später aufzeichnen",
|
||||
"body": "Zeichnen Sie bis zu {{max_duration}} des Meetings auf.",
|
||||
"bodyWithoutMaxDuration": "Zeichnen Sie Ihr Meeting unbegrenzt auf.",
|
||||
"linkMore": "Mehr erfahren",
|
||||
"details": {
|
||||
"receiver": "Die Aufzeichnung wird an den Organisator und die Mitorganisatoren gesendet.",
|
||||
"destination": "Diese Aufzeichnung wird vorübergehend auf unseren Servern gespeichert",
|
||||
"loading": "Aufzeichnung wird gestartet",
|
||||
"linkMore": "Mehr erfahren"
|
||||
"linkMore": "Mehr erfahren",
|
||||
"transcription": "Diese Aufzeichnung transkribieren"
|
||||
},
|
||||
"button": {
|
||||
"start": "Meeting-Aufzeichnung starten",
|
||||
"stop": "Meeting-Aufzeichnung beenden",
|
||||
"saving": "Wird gespeichert…"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Zugriff eingeschränkt",
|
||||
@@ -370,15 +379,6 @@
|
||||
"body": "Nur der Ersteller der Besprechung oder ein Administrator kann die Aufzeichnung starten. Melden Sie sich an, um Ihre Berechtigungen zu überprüfen."
|
||||
}
|
||||
},
|
||||
"stopping": {
|
||||
"heading": "Daten werden gespeichert…",
|
||||
"body": "Sie können das Meeting verlassen, wenn Sie möchten; die Aufzeichnung wird automatisch beendet."
|
||||
},
|
||||
"stop": {
|
||||
"heading": "Aufzeichnung läuft…",
|
||||
"body": "Du erhältst das Ergebnis per E-Mail, sobald die Aufzeichnung abgeschlossen ist.",
|
||||
"button": "Aufzeichnung stoppen"
|
||||
},
|
||||
"alert": {
|
||||
"title": "Aufzeichnung fehlgeschlagen",
|
||||
"body": {
|
||||
|
||||
@@ -354,12 +354,21 @@
|
||||
}
|
||||
},
|
||||
"screenRecording": {
|
||||
"start": {
|
||||
"heading": "Record this call",
|
||||
"body": "Record this call to watch it later {{duration_message}} and receive the video recording by email.",
|
||||
"button": "Start recording",
|
||||
"loading": "Recording starting",
|
||||
"linkMore": "Learn more"
|
||||
"heading": "Record this call for later",
|
||||
"body": "Record up to {{max_duration}} of meeting.",
|
||||
"bodyWithoutMaxDuration": "Record your meeting without limit.",
|
||||
"linkMore": "Learn more",
|
||||
"details": {
|
||||
"receiver": "The recording will be sent to the host and co-hosts.",
|
||||
"destination": "This recording will be temporarily stored on our servers",
|
||||
"loading": "Starting recording",
|
||||
"linkMore": "Learn more",
|
||||
"transcription": "Transcribe this recording"
|
||||
},
|
||||
"button": {
|
||||
"start": "Start recording the meeting",
|
||||
"stop": "Stop recording the meeting",
|
||||
"saving": "Saving…"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Restricted Access",
|
||||
@@ -370,15 +379,6 @@
|
||||
"body": "Only the meeting creator or an admin can start screen recording. Log in to verify your permissions."
|
||||
}
|
||||
},
|
||||
"stopping": {
|
||||
"heading": "Saving your data…",
|
||||
"body": "You can leave the meeting if you wish; the recording will finish automatically."
|
||||
},
|
||||
"stop": {
|
||||
"heading": "Recording in progress…",
|
||||
"body": "You will receive the result by email once the recording is complete.",
|
||||
"button": "Stop recording"
|
||||
},
|
||||
"alert": {
|
||||
"title": "Recording Failed",
|
||||
"body": {
|
||||
|
||||
@@ -354,12 +354,21 @@
|
||||
}
|
||||
},
|
||||
"screenRecording": {
|
||||
"start": {
|
||||
"heading": "Enregistrer cet appel",
|
||||
"body": "Enregistrez cet appel pour plus tard {{duration_message}} et recevez l'enregistrement vidéo par mail.",
|
||||
"button": "Démarrer l'enregistrement",
|
||||
"heading": "Enregistrez cet appel pour plus tard",
|
||||
"body": "Enregistrez jusqu'à {{max_duration}} de réunion.",
|
||||
"bodyWithoutMaxDuration": "Enregistrez votre réunion sans limite.",
|
||||
"linkMore": "En savoir plus",
|
||||
"details": {
|
||||
"receiver": "L'enregistrement sera envoyé à l'organisateur et aux coorganisateurs.",
|
||||
"destination": "Cet enregistrement sera conservé temporairement sur nos serveurs",
|
||||
"loading": "Démarrage de l'enregistrement",
|
||||
"linkMore": "En savoir plus"
|
||||
"linkMore": "En savoir plus",
|
||||
"transcription": "Transcrire cet enregistrement"
|
||||
},
|
||||
"button": {
|
||||
"start": "Commencer à enregistrer la réunion",
|
||||
"stop": "Arrêter d'enregistrer la réunion",
|
||||
"saving": "Sauvegarde…"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Accès restreint",
|
||||
@@ -370,15 +379,6 @@
|
||||
"body": "Seul le créateur de la réunion ou un administrateur peut démarrer l'enregistrement. Connectez-vous pour vérifier vos autorisations."
|
||||
}
|
||||
},
|
||||
"stopping": {
|
||||
"heading": "Sauvegarde de vos données…",
|
||||
"body": "Vous pouvez quitter la réunion si vous le souhaitez, la sauvegarde se terminera automatiquement."
|
||||
},
|
||||
"stop": {
|
||||
"heading": "Enregistrement en cours …",
|
||||
"body": "Vous recevrez le resultat par email une fois l'enregistrement terminé.",
|
||||
"button": "Arrêter l'enregistrement"
|
||||
},
|
||||
"alert": {
|
||||
"title": "Échec de l'enregistrement",
|
||||
"body": {
|
||||
|
||||
@@ -314,7 +314,7 @@
|
||||
"body": "Transcribeer tot {{max_duration}} aan vergadertijd.",
|
||||
"bodyWithoutMaxDuration": "Transcribeer uw vergadering zonder limiet.",
|
||||
"details": {
|
||||
"receiver": "Het transcript wordt verzonden naar de organisator en medeorganisatoren.",
|
||||
"receiver": "Het transcript wordt verzonden naar de host en de co-host.",
|
||||
"destination": "Er wordt een nieuw document aangemaakt op",
|
||||
"destinationUnknown": "Een nieuw document wordt aangemaakt",
|
||||
"language": "Vergadertalen:",
|
||||
@@ -354,12 +354,21 @@
|
||||
}
|
||||
},
|
||||
"screenRecording": {
|
||||
"start": {
|
||||
"heading": "Dit gesprek opnemen",
|
||||
"body": "Neem dit gesprek op om het later terug te kijken {{duration_message}}. Je ontvangt de video-opname per e-mail.",
|
||||
"button": "Opname starten",
|
||||
"loading": "Opname gestarten",
|
||||
"linkMore": "Meer informatie"
|
||||
"heading": "Neem dit gesprek op voor later",
|
||||
"body": "Neem tot {{max_duration}} van de vergadering op.",
|
||||
"bodyWithoutMaxDuration": "Neem je vergadering onbeperkt op.",
|
||||
"linkMore": "Meer informatie",
|
||||
"details": {
|
||||
"receiver": "De opname wordt verzonden naar de organisator en co-organisatoren.",
|
||||
"destination": "Deze opname wordt tijdelijk op onze servers bewaard",
|
||||
"loading": "Opname wordt gestart",
|
||||
"linkMore": "Meer informatie",
|
||||
"transcription": "Transcribeer deze opname"
|
||||
},
|
||||
"button": {
|
||||
"start": "Start met opnemen van de vergadering",
|
||||
"stop": "Stop met opnemen van de vergadering",
|
||||
"saving": "Bezig met opslaan…"
|
||||
},
|
||||
"notAdminOrOwner": {
|
||||
"heading": "Toegang beperkt",
|
||||
@@ -370,15 +379,6 @@
|
||||
"body": "Alleen de maker van de vergadering of een beheerder kan de opname starten. Log in om uw machtigingen te controleren."
|
||||
}
|
||||
},
|
||||
"stopping": {
|
||||
"heading": "Uw gegevens worden opgeslagen…",
|
||||
"body": "U kunt de vergadering verlaten als u dat wilt; de opname wordt automatisch voltooid."
|
||||
},
|
||||
"stop": {
|
||||
"heading": "Opname bezig …",
|
||||
"body": "Je ontvangt het resultaat per e-mail zodra de opname is voltooid.",
|
||||
"button": "Opname stoppen"
|
||||
},
|
||||
"alert": {
|
||||
"title": "Opname mislukt",
|
||||
"body": {
|
||||
|
||||
Reference in New Issue
Block a user