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