🚸(frontend) notify all participants when recording/transcription stops

Broadcast limit-reached notifications to all room participants, not just
owner, to ensure everyone knows recording has stopped due to duration
limits.
This commit is contained in:
lebaudantoine
2025-07-16 12:11:20 +02:00
committed by aleb_the_flash
parent 7c631bb76f
commit d075d60d19
12 changed files with 47 additions and 21 deletions

View File

@@ -72,29 +72,33 @@ export const MainNotificationToast = () => {
) => { ) => {
const notification = decodeNotificationDataReceived(payload) const notification = decodeNotificationDataReceived(payload)
if (!participant || !notification) return if (!notification) return
switch (notification.type) { switch (notification.type) {
case NotificationType.ParticipantMuted: case NotificationType.ParticipantMuted:
toastQueue.add( if (participant) {
{ toastQueue.add(
participant, {
type: NotificationType.ParticipantMuted, participant,
}, type: NotificationType.ParticipantMuted,
{ timeout: NotificationDuration.ALERT } },
) { timeout: NotificationDuration.ALERT }
)
}
break break
case NotificationType.ReactionReceived: case NotificationType.ReactionReceived:
if (notification.data?.emoji) if (notification.data?.emoji && participant)
handleEmoji(notification.data.emoji, participant) handleEmoji(notification.data.emoji, participant)
break break
case NotificationType.TranscriptionStarted: case NotificationType.TranscriptionStarted:
case NotificationType.TranscriptionStopped: case NotificationType.TranscriptionStopped:
case NotificationType.ScreenRecordingStarted: case NotificationType.ScreenRecordingStarted:
case NotificationType.ScreenRecordingStopped: case NotificationType.ScreenRecordingStopped:
case NotificationType.TranscriptionLimitReached:
case NotificationType.ScreenRecordingLimitReached:
toastQueue.add( toastQueue.add(
{ {
participant,
type: notification.type, type: notification.type,
}, },
{ timeout: NotificationDuration.ALERT } { timeout: NotificationDuration.ALERT }

View File

@@ -19,10 +19,14 @@ export function ToastAnyRecording({ state, ...props }: ToastProps) {
return 'transcript.started' return 'transcript.started'
case NotificationType.TranscriptionStopped: case NotificationType.TranscriptionStopped:
return 'transcript.stopped' return 'transcript.stopped'
case NotificationType.TranscriptionLimitReached:
return 'transcript.limitReached'
case NotificationType.ScreenRecordingStarted: case NotificationType.ScreenRecordingStarted:
return 'screenRecording.started' return 'screenRecording.started'
case NotificationType.ScreenRecordingStopped: case NotificationType.ScreenRecordingStopped:
return 'screenRecording.stopped' return 'screenRecording.stopped'
case NotificationType.ScreenRecordingLimitReached:
return 'screenRecording.limitReached'
default: default:
return return
} }
@@ -40,7 +44,7 @@ export function ToastAnyRecording({ state, ...props }: ToastProps) {
gap={0} gap={0}
> >
{t(key, { {t(key, {
name: participant.name, name: participant?.name,
})} })}
</HStack> </HStack>
</StyledToastContainer> </StyledToastContainer>

View File

@@ -29,6 +29,9 @@ export function ToastJoined({ state, ...props }: ToastProps) {
) )
const layoutContext = useMaybeLayoutContext() const layoutContext = useMaybeLayoutContext()
const participant = props.toast.content.participant const participant = props.toast.content.participant
if (!participant) return
const trackReference = { const trackReference = {
participant, participant,
publication: participant.getTrackPublication(Source.Camera), publication: participant.getTrackPublication(Source.Camera),

View File

@@ -27,7 +27,7 @@ export function ToastMessageReceived({ state, ...props }: ToastProps) {
} }
}, [isChatOpen, toast, state]) }, [isChatOpen, toast, state])
if (isChatOpen) return null if (isChatOpen || !participant) return null
return ( return (
<StyledToastContainer {...toastProps} ref={ref}> <StyledToastContainer {...toastProps} ref={ref}>

View File

@@ -10,6 +10,9 @@ export function ToastMuted({ state, ...props }: ToastProps) {
const ref = useRef(null) const ref = useRef(null)
const { toastProps, contentProps } = useToast(props, state, ref) const { toastProps, contentProps } = useToast(props, state, ref)
const participant = props.toast.content.participant const participant = props.toast.content.participant
if (!participant) return
return ( return (
<StyledToastContainer {...toastProps} ref={ref}> <StyledToastContainer {...toastProps} ref={ref}>
<HStack <HStack

View File

@@ -5,7 +5,7 @@ import { Participant } from 'livekit-client'
import { NotificationType } from '../NotificationType' import { NotificationType } from '../NotificationType'
export interface ToastData { export interface ToastData {
participant: Participant participant?: Participant
type: NotificationType type: NotificationType
message?: string message?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -20,6 +20,8 @@ export function ToastRaised({ state, ...props }: ToastProps) {
const participant = props.toast.content.participant const participant = props.toast.content.participant
const { isParticipantsOpen, toggleParticipants } = useSidePanel() const { isParticipantsOpen, toggleParticipants } = useSidePanel()
if (!participant) return
return ( return (
<StyledToastContainer {...toastProps} ref={ref}> <StyledToastContainer {...toastProps} ref={ref}>
<HStack <HStack

View File

@@ -40,8 +40,10 @@ const renderToast = (
case NotificationType.TranscriptionStarted: case NotificationType.TranscriptionStarted:
case NotificationType.TranscriptionStopped: case NotificationType.TranscriptionStopped:
case NotificationType.TranscriptionLimitReached:
case NotificationType.ScreenRecordingStarted: case NotificationType.ScreenRecordingStarted:
case NotificationType.ScreenRecordingStopped: case NotificationType.ScreenRecordingStopped:
case NotificationType.ScreenRecordingLimitReached:
return <ToastAnyRecording key={toast.key} toast={toast} state={state} /> return <ToastAnyRecording key={toast.key} toast={toast} state={state} />
case NotificationType.RecordingSaving: case NotificationType.RecordingSaving:

View File

@@ -24,11 +24,13 @@
}, },
"transcript": { "transcript": {
"started": "{{name}} hat die Transkription des Treffens gestartet.", "started": "{{name}} hat die Transkription des Treffens gestartet.",
"stopped": "{{name}} hat die Transkription des Treffens gestoppt." "stopped": "{{name}} hat die Transkription des Treffens gestoppt.",
"limitReached": "Die Transkription hat die maximal zulässige Dauer überschritten und wird automatisch gespeichert."
}, },
"screenRecording": { "screenRecording": {
"started": "{{name}} hat die Aufzeichnung des Treffens gestartet.", "started": "{{name}} hat die Aufzeichnung des Treffens gestartet.",
"stopped": "{{name}} hat die Aufzeichnung des Treffens gestoppt." "stopped": "{{name}} hat die Aufzeichnung des Treffens gestoppt.",
"limitReached": "Die Aufzeichnung hat die maximal zulässige Dauer überschritten und wird automatisch gespeichert."
}, },
"recordingSave": { "recordingSave": {
"transcript": { "transcript": {

View File

@@ -24,11 +24,13 @@
}, },
"transcript": { "transcript": {
"started": "{{name}} started the meeting transcription.", "started": "{{name}} started the meeting transcription.",
"stopped": "{{name}} stopped the meeting transcription." "stopped": "{{name}} stopped the meeting transcription.",
"limitReached": "The transcription has exceeded the maximum allowed duration and will be automatically saved."
}, },
"screenRecording": { "screenRecording": {
"started": "{{name}} started the meeting recording.", "started": "{{name}} started the meeting recording.",
"stopped": "{{name}} stopped the meeting recording." "stopped": "{{name}} stopped the meeting recording.",
"limitReached": "The recording has exceeded the maximum allowed duration and will be automatically saved."
}, },
"recordingSave": { "recordingSave": {
"transcript": { "transcript": {

View File

@@ -24,11 +24,13 @@
}, },
"transcript": { "transcript": {
"started": "{{name}} a démarré la transcription de la réunion.", "started": "{{name}} a démarré la transcription de la réunion.",
"stopped": "{{name}} a arrêté la transcription de la réunion." "stopped": "{{name}} a arrêté la transcription de la réunion.",
"limitReached": "La transcription a dépassé la durée maximale autorisée, elle va être automatiquement sauvegardée."
}, },
"screenRecording": { "screenRecording": {
"started": "{{name}} a démarré l'enregistrement de la réunion.", "started": "{{name}} a démarré l'enregistrement de la réunion.",
"stopped": "{{name}} a arrêté l'enregistrement de la réunion." "stopped": "{{name}} a arrêté l'enregistrement de la réunion.",
"limitReached": "L'enregistrement a dépassé la durée maximale autorisée, il va être automatiquement sauvegardé."
}, },
"recordingSave": { "recordingSave": {
"transcript": { "transcript": {

View File

@@ -24,11 +24,13 @@
}, },
"transcript": { "transcript": {
"started": "{{name}} is de transcriptie van de vergadering gestart.", "started": "{{name}} is de transcriptie van de vergadering gestart.",
"stopped": "{{name}} heeft de transcriptie van de vergadering gestopt." "stopped": "{{name}} heeft de transcriptie van de vergadering gestopt.",
"limitReached": "De transcriptie heeft de maximaal toegestane duur overschreden en wordt automatisch opgeslagen."
}, },
"screenRecording": { "screenRecording": {
"started": "{{name}} is begonnen met het opnemen van de vergadering.", "started": "{{name}} is begonnen met het opnemen van de vergadering.",
"stopped": "{{name}} is gestopt met het opnemen van de vergadering." "stopped": "{{name}} is gestopt met het opnemen van de vergadering.",
"limitReached": "De opname heeft de maximaal toegestane duur overschreden en wordt automatisch opgeslagen."
}, },
"recordingSave": { "recordingSave": {
"transcript": { "transcript": {