(frontend) introde the Screen Recording side panel

Offer one new tool to user, the screen recording side panel.
This commit is contained in:
lebaudantoine
2025-04-07 12:48:05 +02:00
committed by aleb_the_flash
parent a22d052f46
commit 468d09dc3b
8 changed files with 261 additions and 10 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

View File

@@ -0,0 +1,163 @@
import { A, Button, Div, Text } from '@/primitives'
import fourthSlide from '@/assets/intro-slider/4_record.png'
import { css } from '@/styled-system/css'
import { useRoomId } from '@/features/rooms/livekit/hooks/useRoomId'
import { useRoomContext } from '@livekit/components-react'
import {
RecordingMode,
useStartRecording,
} from '@/features/rooms/api/startRecording'
import { useStopRecording } from '@/features/rooms/api/stopRecording'
import { useEffect, useMemo, useState } from 'react'
import { RoomEvent } from 'livekit-client'
import { useTranslation } from 'react-i18next'
import { NotificationPayload } from '@/features/notifications/NotificationPayload'
import { NotificationType } from '@/features/notifications/NotificationType'
import { useSnapshot } from 'valtio/index'
import { RecordingStatus, recordingStore } from '@/stores/recording'
import { CRISP_HELP_ARTICLE_RECORDING } from '@/utils/constants'
export const ScreenRecording = () => {
const [isLoading, setIsLoading] = useState(false)
const { t } = useTranslation('rooms', { keyPrefix: 'screenRecording' })
const roomId = useRoomId()
const { mutateAsync: startRecordingRoom } = useStartRecording()
const { mutateAsync: stopRecordingRoom } = useStopRecording()
const recordingSnap = useSnapshot(recordingStore)
const room = useRoomContext()
useEffect(() => {
const handleRecordingStatusChanged = () => {
setIsLoading(false)
}
room.on(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
return () => {
room.off(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
}
}, [room])
const notifyParticipant = async (status: NotificationType) => {
const encoder = new TextEncoder()
const payload: NotificationPayload = {
type: status,
}
const data = encoder.encode(JSON.stringify(payload))
await room.localParticipant.publishData(data, {
reliable: true,
})
}
const handleTranscript = async () => {
if (!roomId) {
console.warn('No room ID found')
return
}
try {
setIsLoading(true)
if (room.isRecording) {
await stopRecordingRoom({ id: roomId })
await notifyParticipant(NotificationType.ScreenRecordingStopped)
recordingStore.status = RecordingStatus.SCREEN_RECORDING_STOPPING
} else {
await startRecordingRoom({
id: roomId,
mode: RecordingMode.ScreenRecording,
})
await notifyParticipant(NotificationType.ScreenRecordingStarted)
recordingStore.status = RecordingStatus.SCREEN_RECORDING_STARTING
}
} catch (error) {
console.error('Failed to handle transcript:', error)
setIsLoading(false)
}
}
const isDisabled = useMemo(
() =>
isLoading ||
recordingSnap.status == RecordingStatus.SCREEN_RECORDING_STARTING ||
recordingSnap.status == RecordingStatus.SCREEN_RECORDING_STOPPING,
[isLoading, recordingSnap]
)
return (
<Div
display="flex"
overflowY="scroll"
padding="0 1.5rem"
flexGrow={1}
flexDirection="column"
alignItems="center"
>
<img
src={fourthSlide}
alt={''}
className={css({
minHeight: '309px',
marginBottom: '1rem',
})}
/>
{room.isRecording ? (
<>
<Text>{t('stop.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('stop.body')}
</Text>
<Button
isDisabled={isDisabled}
onPress={() => handleTranscript()}
data-attr="stop-transcript"
size="sm"
variant="tertiary"
>
{t('stop.button')}
</Button>
</>
) : (
<>
<Text>{t('start.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
maxWidth: '90%',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('start.body')} <br />{' '}
<A href={CRISP_HELP_ARTICLE_RECORDING} target="_blank">
{t('start.linkMore')}
</A>
</Text>
<Button
isDisabled={isDisabled}
onPress={() => handleTranscript()}
data-attr="start-transcript"
size="sm"
variant="tertiary"
>
{t('start.button')}
</Button>
</>
)}
</Div>
)
}

View File

@@ -5,9 +5,10 @@ import { useTranslation } from 'react-i18next'
import { CRISP_HELP_ARTICLE_MORE_TOOLS } from '@/utils/constants'
import { ReactNode } from 'react'
import { Transcript } from './Transcript'
import { RiFileTextFill } from '@remixicon/react'
import { useSidePanel } from '../hooks/useSidePanel'
import { useIsRecordingModeEnabled } from '../hooks/useIsRecordingModeEnabled'
import { RiFileTextFill, RiLiveFill } from '@remixicon/react'
import { SubPanelId, useSidePanel } from '../hooks/useSidePanel'
import { ScreenRecording } from './ScreenRecording'
import { RecordingMode } from '@/features/rooms/api/startRecording'
export interface ToolsButtonProps {
@@ -69,14 +70,24 @@ const ToolButton = ({
}
export const Tools = () => {
const { openTranscript, isTranscriptOpen } = useSidePanel()
const { openTranscript, openScreenRecording, activeSubPanelId } =
useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
const isTranscriptEnabled = useIsRecordingModeEnabled(
RecordingMode.Transcript
)
if (isTranscriptOpen && isTranscriptEnabled) {
return <Transcript />
const isScreenRecordingEnabled = useIsRecordingModeEnabled(
RecordingMode.ScreenRecording
)
switch (activeSubPanelId) {
case SubPanelId.TRANSCRIPT:
return <Transcript />
case SubPanelId.SCREEN_RECORDING:
return <ScreenRecording />
default:
break
}
return (
@@ -111,6 +122,14 @@ export const Tools = () => {
onPress={() => openTranscript()}
/>
)}
{isScreenRecordingEnabled && (
<ToolButton
icon={<RiLiveFill size={24} color="white" />}
title={t('tools.screenRecording.title')}
description={t('tools.screenRecording.body')}
onPress={() => openScreenRecording()}
/>
)}
</Div>
)
}

View File

@@ -11,6 +11,7 @@ export enum PanelId {
export enum SubPanelId {
TRANSCRIPT = 'transcript',
SCREEN_RECORDING = 'screenRecording',
}
export const useSidePanel = () => {
@@ -24,6 +25,7 @@ export const useSidePanel = () => {
const isToolsOpen = activePanelId == PanelId.TOOLS
const isAdminOpen = activePanelId == PanelId.ADMIN
const isTranscriptOpen = activeSubPanelId == SubPanelId.TRANSCRIPT
const isScreenRecordingOpen = activeSubPanelId == SubPanelId.SCREEN_RECORDING
const isSidePanelOpen = !!activePanelId
const isSubPanelOpen = !!activeSubPanelId
@@ -57,6 +59,11 @@ export const useSidePanel = () => {
layoutStore.activePanelId = PanelId.TOOLS
}
const openScreenRecording = () => {
layoutStore.activeSubPanelId = SubPanelId.SCREEN_RECORDING
layoutStore.activePanelId = PanelId.TOOLS
}
return {
activePanelId,
activeSubPanelId,
@@ -66,6 +73,7 @@ export const useSidePanel = () => {
toggleTools,
toggleAdmin,
openTranscript,
openScreenRecording,
isSubPanelOpen,
isChatOpen,
isParticipantsOpen,
@@ -74,5 +82,6 @@ export const useSidePanel = () => {
isToolsOpen,
isAdminOpen,
isTranscriptOpen,
isScreenRecordingOpen,
}
}

View File

@@ -168,6 +168,7 @@
"effects": "Effects",
"chat": "Messages in the chat",
"transcript": "Transcription",
"screenRecording": "Recording",
"admin": "Admin settings",
"tools": "More tools"
},
@@ -175,7 +176,8 @@
"participants": "participants",
"effects": "effects",
"chat": "messages",
"transcript": "Transcription",
"transcript": "transcription",
"screenRecording": "recording",
"admin": "admin settings",
"tools": "more tools"
},
@@ -190,7 +192,11 @@
"tools": {
"transcript": {
"title": "Transcription",
"body": "Transcribe your meeting for later"
"body": "Keep a written record of your meeting."
},
"screenRecording": {
"title": "Recording",
"body": "Record your meeting to watch it again whenever you like."
}
}
},
@@ -212,6 +218,19 @@
"button": "Sign up"
}
},
"screenRecording": {
"start": {
"heading": "Record this call",
"body": "Record this call to watch it later and receive the video recording by email.",
"button": "Start recording",
"linkMore": "Learn more"
},
"stop": {
"heading": "Recording in progress…",
"body": "You will receive the result by email once the recording is complete.",
"button": "Stop recording"
}
},
"admin": {
"description": "These organizer settings allow you to maintain control of your meeting. Only organizers can access these controls.",
"access": {

View File

@@ -168,6 +168,7 @@
"effects": "Effets",
"chat": "Messages dans l'appel",
"transcript": "Transcription",
"screenRecording": "Enregistrement",
"admin": "Commandes de l'organisateur",
"tools": "Plus d'outils"
},
@@ -176,6 +177,7 @@
"effects": "les effets",
"chat": "les messages",
"transcript": "transcription",
"screenRecording": "enregistrement",
"admin": "commandes de l'organisateur",
"tools": "plus d'outils"
},
@@ -190,7 +192,11 @@
"tools": {
"transcript": {
"title": "Transcription",
"body": "Transcrivez votre réunion pour plus tard"
"body": "Conservez une trace écrite de votre réunion."
},
"screenRecording": {
"title": "Enregistrement",
"body": "Enregistrez votre réunion pour la revoir quand vous le souhaitez."
}
}
},
@@ -212,6 +218,19 @@
"button": "Inscrivez-vous"
}
},
"screenRecording": {
"start": {
"heading": "Enregistrer cet appel",
"body": "Enregistrez cet appel pour plus tard et recevez l'enregistrement vidéo par mail.",
"button": "Démarrer l'enregistrement",
"linkMore": "En savoir plus"
},
"stop": {
"heading": "Enregistrement en cours …",
"body": "Vous recevrez le resultat par email une fois l'enregistrement terminé.",
"button": "Arrêter l'enregistrement"
}
},
"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

@@ -168,6 +168,7 @@
"effects": "Effecten",
"chat": "Berichten in de chat",
"transcript": "Transcriptie",
"screenRecording": "Schermopname",
"admin": "Beheerdersbediening",
"tools": "Meer tools"
},
@@ -175,7 +176,8 @@
"participants": "deelnemers",
"effects": "effecten",
"chat": "berichten",
"transcript": "transcriptie",
"screenRecording": "transcriptie",
"transcript": "schermopname",
"admin": "beheerdersbediening",
"tools": "meer tools"
},
@@ -190,7 +192,11 @@
"tools": {
"transcript": {
"title": "Transcriptie",
"body": "Transcribeer je vergadering voor later"
"body": "Bewaar een schriftelijk verslag van je vergadering."
},
"screenRecording": {
"title": "Opname",
"body": "Neem je vergadering op om die later opnieuw te bekijken."
}
}
},
@@ -212,6 +218,19 @@
"button": "Aanmelden"
}
},
"screenRecording": {
"start": {
"heading": "Dit gesprek opnemen",
"body": "Neem dit gesprek op om het later terug te kijken. Je ontvangt de video-opname per e-mail.",
"button": "Opname starten",
"linkMore": "Meer informatie"
},
"stop": {
"heading": "Opname bezig …",
"body": "Je ontvangt het resultaat per e-mail zodra de opname is voltooid.",
"button": "Opname stoppen"
}
},
"admin": {
"description": "Deze organisatorinstellingen geven u controle over uw vergadering. Alleen organisatoren hebben toegang tot deze bedieningselementen.",
"access": {

View File

@@ -9,3 +9,6 @@ export const CRISP_HELP_ARTICLE_MORE_TOOLS =
export const CRISP_HELP_ARTICLE_TRANSCRIPT =
'https://lasuite.crisp.help/fr/article/visio-transcript-1sjq43x' as const
export const CRISP_HELP_ARTICLE_RECORDING =
'https://lasuite.crisp.help/fr/article/visio-enregistrement-wgc8o0' as const