🚸(frontend) alert room owner when recording exceeds max duration
Display notification to prevent silent recording failures. Shows configured max duration in proper locale if backend provides the limit. Prevents users from missing recording termination.
This commit is contained in:
committed by
aleb_the_flash
parent
59cd1f766a
commit
7c631bb76f
15
src/frontend/package-lock.json
generated
15
src/frontend/package-lock.json
generated
@@ -18,6 +18,7 @@
|
||||
"@timephy/rnnoise-wasm": "1.0.0",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"hoofd": "1.7.3",
|
||||
"humanize-duration": "3.33.0",
|
||||
"i18next": "25.3.1",
|
||||
"i18next-browser-languagedetector": "8.2.0",
|
||||
"i18next-parser": "9.3.0",
|
||||
@@ -37,6 +38,7 @@
|
||||
"@pandacss/dev": "0.54.0",
|
||||
"@tanstack/eslint-plugin-query": "5.81.2",
|
||||
"@tanstack/react-query-devtools": "5.81.5",
|
||||
"@types/humanize-duration": "3.27.4",
|
||||
"@types/node": "22.16.0",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
@@ -3994,6 +3996,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/humanize-duration": {
|
||||
"version": "3.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.27.4.tgz",
|
||||
"integrity": "sha512-yaf7kan2Sq0goxpbcwTQ+8E9RP6HutFBPv74T/IA/ojcHKhuKVlk2YFYyHhWZeLvZPzzLE3aatuQB4h0iqyyUA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
||||
@@ -6535,6 +6544,12 @@
|
||||
"entities": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/humanize-duration": {
|
||||
"version": "3.33.0",
|
||||
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.33.0.tgz",
|
||||
"integrity": "sha512-vYJX7BSzn7EQ4SaP2lPYVy+icHDppB6k7myNeI3wrSRfwMS5+BHyGgzpHR0ptqJ2AQ6UuIKrclSg5ve6Ci4IAQ==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.3.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.1.tgz",
|
||||
|
||||
@@ -23,13 +23,14 @@
|
||||
"@timephy/rnnoise-wasm": "1.0.0",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"hoofd": "1.7.3",
|
||||
"humanize-duration": "3.33.0",
|
||||
"i18next": "25.3.1",
|
||||
"i18next-browser-languagedetector": "8.2.0",
|
||||
"i18next-parser": "9.3.0",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"libphonenumber-js": "1.12.9",
|
||||
"livekit-client": "2.15.2",
|
||||
"posthog-js": "1.256.2",
|
||||
"libphonenumber-js": "1.12.9",
|
||||
"react": "18.3.1",
|
||||
"react-aria-components": "1.10.1",
|
||||
"react-dom": "18.3.1",
|
||||
@@ -42,6 +43,7 @@
|
||||
"@pandacss/dev": "0.54.0",
|
||||
"@tanstack/eslint-plugin-query": "5.81.2",
|
||||
"@tanstack/react-query-devtools": "5.81.5",
|
||||
"@types/humanize-duration": "3.27.4",
|
||||
"@types/node": "22.16.0",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
|
||||
@@ -8,7 +8,9 @@ export enum NotificationType {
|
||||
ParticipantWaiting = 'participantWaiting',
|
||||
TranscriptionStarted = 'transcriptionStarted',
|
||||
TranscriptionStopped = 'transcriptionStopped',
|
||||
TranscriptionLimitReached = 'transcriptionLimitReached',
|
||||
ScreenRecordingStarted = 'screenRecordingStarted',
|
||||
ScreenRecordingStopped = 'screenRecordingStopped',
|
||||
ScreenRecordingLimitReached = 'screenRecordingLimitReached',
|
||||
RecordingSaving = 'recordingSaving',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Dialog, P } from '@/primitives'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import humanizeDuration from 'humanize-duration'
|
||||
|
||||
export const LimitReachedAlertDialog = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation('rooms', {
|
||||
keyPrefix: 'recordingStateToast.limitReachedAlert',
|
||||
})
|
||||
const { data } = useConfig()
|
||||
return (
|
||||
<Dialog isOpen={isOpen} role="alertdialog" title={t('title')}>
|
||||
<P>
|
||||
{t('description', {
|
||||
duration_message: data?.recording?.max_duration
|
||||
? t('durationMessage', {
|
||||
duration: humanizeDuration(data?.recording?.max_duration, {
|
||||
language: i18n.language,
|
||||
}),
|
||||
})
|
||||
: '',
|
||||
})}
|
||||
</P>
|
||||
<HStack gap={1}>
|
||||
<Button variant="text" size="sm" onPress={onClose}>
|
||||
{t('button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSnapshot } from 'valtio/index'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
import { Spinner } from '@/primitives/Spinner'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Text } from '@/primitives'
|
||||
import { RemoteParticipant, RoomEvent } from 'livekit-client'
|
||||
import { RoomEvent } from 'livekit-client'
|
||||
import { decodeNotificationDataReceived } from '@/features/notifications/utils'
|
||||
import { NotificationType } from '@/features/notifications/NotificationType'
|
||||
import { RecordingStatus, recordingStore } from '@/stores/recording'
|
||||
@@ -18,14 +18,18 @@ import {
|
||||
import { FeatureFlags } from '@/features/analytics/enums'
|
||||
import { Button as RACButton } from 'react-aria-components'
|
||||
import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
|
||||
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
|
||||
import { LimitReachedAlertDialog } from './LimitReachedAlertDialog'
|
||||
|
||||
export const RecordingStateToast = () => {
|
||||
const { t } = useTranslation('rooms', {
|
||||
keyPrefix: 'recordingStateToast',
|
||||
})
|
||||
const room = useRoomContext()
|
||||
const isAdminOrOwner = useIsAdminOrOwner()
|
||||
|
||||
const { openTranscript, openScreenRecording } = useSidePanel()
|
||||
const [isAlertOpen, setIsAlertOpen] = useState(false)
|
||||
|
||||
const recordingSnap = useSnapshot(recordingStore)
|
||||
|
||||
@@ -53,13 +57,10 @@ export const RecordingStateToast = () => {
|
||||
}, [room.isRecording])
|
||||
|
||||
useEffect(() => {
|
||||
const handleDataReceived = (
|
||||
payload: Uint8Array,
|
||||
participant?: RemoteParticipant
|
||||
) => {
|
||||
const handleDataReceived = (payload: Uint8Array) => {
|
||||
const notification = decodeNotificationDataReceived(payload)
|
||||
|
||||
if (!participant || !notification) return
|
||||
if (!notification) return
|
||||
|
||||
switch (notification.type) {
|
||||
case NotificationType.TranscriptionStarted:
|
||||
@@ -68,12 +69,20 @@ export const RecordingStateToast = () => {
|
||||
case NotificationType.TranscriptionStopped:
|
||||
recordingStore.status = RecordingStatus.TRANSCRIPT_STOPPING
|
||||
break
|
||||
case NotificationType.TranscriptionLimitReached:
|
||||
if (isAdminOrOwner) setIsAlertOpen(true)
|
||||
recordingStore.status = RecordingStatus.TRANSCRIPT_STOPPING
|
||||
break
|
||||
case NotificationType.ScreenRecordingStarted:
|
||||
recordingStore.status = RecordingStatus.SCREEN_RECORDING_STARTING
|
||||
break
|
||||
case NotificationType.ScreenRecordingStopped:
|
||||
recordingStore.status = RecordingStatus.SCREEN_RECORDING_STOPPING
|
||||
break
|
||||
case NotificationType.ScreenRecordingLimitReached:
|
||||
if (isAdminOrOwner) setIsAlertOpen(true)
|
||||
recordingStore.status = RecordingStatus.SCREEN_RECORDING_STOPPING
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
@@ -100,7 +109,7 @@ export const RecordingStateToast = () => {
|
||||
room.off(RoomEvent.DataReceived, handleDataReceived)
|
||||
room.off(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
|
||||
}
|
||||
}, [room, recordingSnap])
|
||||
}, [room, recordingSnap, setIsAlertOpen, isAdminOrOwner])
|
||||
|
||||
const key = useMemo(() => {
|
||||
switch (recordingSnap.status) {
|
||||
@@ -119,7 +128,14 @@ export const RecordingStateToast = () => {
|
||||
}
|
||||
}, [recordingSnap])
|
||||
|
||||
if (!key) return
|
||||
if (!key)
|
||||
return isAdminOrOwner ? (
|
||||
<LimitReachedAlertDialog
|
||||
isOpen={isAlertOpen}
|
||||
onClose={() => setIsAlertOpen(false)}
|
||||
aria-label="Recording limit exceeded"
|
||||
/>
|
||||
) : null
|
||||
|
||||
const isStarted = key?.includes('started')
|
||||
|
||||
|
||||
@@ -379,6 +379,12 @@
|
||||
},
|
||||
"any": {
|
||||
"started": "Aufzeichnung läuft"
|
||||
},
|
||||
"limitReachedAlert": {
|
||||
"title": "Aufnahmelimit überschritten",
|
||||
"description": "Die Aufnahme hat die zulässige Höchstdauer{{duration_message}} überschritten. Sie wird jetzt automatisch gespeichert.",
|
||||
"durationMessage": " von {{duration}}",
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
"participantTileFocus": {
|
||||
|
||||
@@ -379,6 +379,12 @@
|
||||
},
|
||||
"any": {
|
||||
"started": "Recording in progress"
|
||||
},
|
||||
"limitReachedAlert": {
|
||||
"title": "Recording limit exceeded",
|
||||
"description": "The recording has exceeded the maximum allowed duration{{duration_message}}. It will now be saved automatically.",
|
||||
"durationMessage": " of {{duration}}",
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
"participantTileFocus": {
|
||||
|
||||
@@ -379,6 +379,12 @@
|
||||
},
|
||||
"any": {
|
||||
"started": "Enregistrement en cours"
|
||||
},
|
||||
"limitReachedAlert": {
|
||||
"title": "Limite d'enregistrement dépassée",
|
||||
"description": "L'enregistrement a dépassé la durée maximale autorisée{{duration_message}}. Il va maintenant être automatiquement sauvegardé.",
|
||||
"durationMessage": " de {{duration}}",
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
"participantTileFocus": {
|
||||
|
||||
@@ -379,6 +379,12 @@
|
||||
},
|
||||
"any": {
|
||||
"started": "Opname bezig"
|
||||
},
|
||||
"limitReachedAlert": {
|
||||
"title": "Opnamelimiet overschreden",
|
||||
"description": "De opname heeft de maximaal toegestane duur{{duration_message}} overschreden. Deze wordt nu automatisch opgeslagen.",
|
||||
"durationMessage": " van {{duration}}",
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
"participantTileFocus": {
|
||||
|
||||
Reference in New Issue
Block a user