diff --git a/src/frontend/src/assets/intro-slider/4_record.png b/src/frontend/src/assets/intro-slider/4_record.png new file mode 100644 index 00000000..e74700f3 Binary files /dev/null and b/src/frontend/src/assets/intro-slider/4_record.png differ diff --git a/src/frontend/src/features/rooms/livekit/components/ScreenRecording.tsx b/src/frontend/src/features/rooms/livekit/components/ScreenRecording.tsx new file mode 100644 index 00000000..3168f468 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/components/ScreenRecording.tsx @@ -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 ( +
+ {''} + + {room.isRecording ? ( + <> + {t('stop.heading')} + + {t('stop.body')} + + + + ) : ( + <> + {t('start.heading')} + + {t('start.body')}
{' '} + + {t('start.linkMore')} + +
+ + + )} +
+ ) +} diff --git a/src/frontend/src/features/rooms/livekit/components/Tools.tsx b/src/frontend/src/features/rooms/livekit/components/Tools.tsx index 8320d7b0..6ed14a16 100644 --- a/src/frontend/src/features/rooms/livekit/components/Tools.tsx +++ b/src/frontend/src/features/rooms/livekit/components/Tools.tsx @@ -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 + const isScreenRecordingEnabled = useIsRecordingModeEnabled( + RecordingMode.ScreenRecording + ) + + switch (activeSubPanelId) { + case SubPanelId.TRANSCRIPT: + return + case SubPanelId.SCREEN_RECORDING: + return + default: + break } return ( @@ -111,6 +122,14 @@ export const Tools = () => { onPress={() => openTranscript()} /> )} + {isScreenRecordingEnabled && ( + } + title={t('tools.screenRecording.title')} + description={t('tools.screenRecording.body')} + onPress={() => openScreenRecording()} + /> + )} ) } diff --git a/src/frontend/src/features/rooms/livekit/hooks/useSidePanel.ts b/src/frontend/src/features/rooms/livekit/hooks/useSidePanel.ts index de4e32a7..ed23512e 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/useSidePanel.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/useSidePanel.ts @@ -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, } } diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json index 7681d5e8..4611f2c0 100644 --- a/src/frontend/src/locales/en/rooms.json +++ b/src/frontend/src/locales/en/rooms.json @@ -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": { diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json index 6f5e0f45..b6197da1 100644 --- a/src/frontend/src/locales/fr/rooms.json +++ b/src/frontend/src/locales/fr/rooms.json @@ -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": { diff --git a/src/frontend/src/locales/nl/rooms.json b/src/frontend/src/locales/nl/rooms.json index 76e04210..8cdf2c2e 100644 --- a/src/frontend/src/locales/nl/rooms.json +++ b/src/frontend/src/locales/nl/rooms.json @@ -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": { diff --git a/src/frontend/src/utils/constants.ts b/src/frontend/src/utils/constants.ts index 34a5bba0..546030fa 100644 --- a/src/frontend/src/utils/constants.ts +++ b/src/frontend/src/utils/constants.ts @@ -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