From f38dec6a93b68c64d324130084bc4772cc76980f Mon Sep 17 00:00:00 2001 From: Cyril Date: Mon, 27 Apr 2026 11:04:11 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20show=20room=20notificatio?= =?UTF-8?q?ns=20inside=20the=20PiP=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render the shared toast queue in PiP. --- .../notifications/MainNotificationToast.tsx | 8 +- .../pip/components/DocumentPiPPortal.tsx | 2 +- .../src/features/pip/components/PipView.tsx | 4 + .../pip/components/layouts/PipFocusLayout.tsx | 162 ++++++++-------- .../pip/components/layouts/PipGridLayout.tsx | 164 ++++++++--------- .../pip/components/layouts/PipStage.tsx | 174 +++++++++--------- .../notifications/PipConnectionStateToast.tsx | 52 ++++++ .../notifications/PipNotificationOverlay.tsx | 79 ++++++++ .../components/notifications/PipToastBody.tsx | 119 ++++++++++++ src/frontend/src/locales/de/rooms.json | 8 +- src/frontend/src/locales/en/rooms.json | 8 +- src/frontend/src/locales/fr/rooms.json | 8 +- src/frontend/src/locales/nl/rooms.json | 8 +- 13 files changed, 540 insertions(+), 256 deletions(-) create mode 100644 src/frontend/src/features/pip/components/notifications/PipConnectionStateToast.tsx create mode 100644 src/frontend/src/features/pip/components/notifications/PipNotificationOverlay.tsx create mode 100644 src/frontend/src/features/pip/components/notifications/PipToastBody.tsx diff --git a/src/frontend/src/features/notifications/MainNotificationToast.tsx b/src/frontend/src/features/notifications/MainNotificationToast.tsx index 6e5ea93c..bc17d81e 100644 --- a/src/frontend/src/features/notifications/MainNotificationToast.tsx +++ b/src/frontend/src/features/notifications/MainNotificationToast.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react' +import { useCallback, useEffect, useRef } from 'react' import { useRoomContext } from '@livekit/components-react' import { Participant, RemoteParticipant, RoomEvent } from 'livekit-client' import { ChatMessage, isMobileBrowser } from '@livekit/components-core' @@ -24,12 +24,18 @@ export const MainNotificationToast = () => { const { appendReaction } = useReactions() + // Chat uses keepAlive in multiple SidePanels (main + PiP), so the same + // message event can fire more than once. Track the last id to deduplicate. + const lastChatMsgIdRef = useRef('') + useEffect(() => { const handleChatMessage = ( chatMessage: ChatMessage, participant?: Participant | undefined ) => { if (!participant || participant.isLocal) return + if (chatMessage.id && chatMessage.id === lastChatMsgIdRef.current) return + lastChatMsgIdRef.current = chatMessage.id ?? '' triggerNotificationSound(NotificationType.MessageReceived) toastQueue.add( { diff --git a/src/frontend/src/features/pip/components/DocumentPiPPortal.tsx b/src/frontend/src/features/pip/components/DocumentPiPPortal.tsx index 52326e65..793e18fd 100644 --- a/src/frontend/src/features/pip/components/DocumentPiPPortal.tsx +++ b/src/frontend/src/features/pip/components/DocumentPiPPortal.tsx @@ -170,7 +170,7 @@ export const DocumentPiPPortal = ({ return () => { cancelled = true } - }, [closePiP, isOpen, isSupported, openPiP]) + }, [closePiP, isOpen, isSupported, openPiP, t]) // Focus stays on the trigger; PiP is announced as an auxiliary surface. useEffect(() => { diff --git a/src/frontend/src/features/pip/components/PipView.tsx b/src/frontend/src/features/pip/components/PipView.tsx index bfe1026b..92e39d6d 100644 --- a/src/frontend/src/features/pip/components/PipView.tsx +++ b/src/frontend/src/features/pip/components/PipView.tsx @@ -11,6 +11,8 @@ import { usePipRestoreFocus } from '../hooks/usePipRestoreFocus' import { PipControlBar } from './PipControlBar' import { PipReactionsToolbar } from './PipReactionsToolbar' import { PipStage } from './layouts/PipStage' +import { PipNotificationOverlay } from './notifications/PipNotificationOverlay' +import { PipConnectionStateToast } from './notifications/PipConnectionStateToast' export const PipView = () => { const browserSupportsScreenSharing = supportsScreenSharing() @@ -47,6 +49,8 @@ export const PipView = () => { + + ) } diff --git a/src/frontend/src/features/pip/components/layouts/PipFocusLayout.tsx b/src/frontend/src/features/pip/components/layouts/PipFocusLayout.tsx index 9cd10aea..5cd54202 100644 --- a/src/frontend/src/features/pip/components/layouts/PipFocusLayout.tsx +++ b/src/frontend/src/features/pip/components/layouts/PipFocusLayout.tsx @@ -1,81 +1,81 @@ -import { memo } from 'react' -import type { TrackReferenceOrPlaceholder } from '@livekit/components-core' -import { styled } from '@/styled-system/jsx' -import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile' -import { getTrackKey } from '../../utils/pipTrackSelection' - -type PipFocusLayoutProps = { - mainTrack: TrackReferenceOrPlaceholder - thumbnailTrack?: TrackReferenceOrPlaceholder -} - -/** - * Focus layout used when 1-2 tracks are visible in the PiP window. - * - * The main tile is letterboxed (object-fit: contain) so the camera is - * never stretched to a non-video aspect and leaves dark padding - * above/below when the window shape doesn't match the source. - * The thumbnail keeps the usual cover fill. - */ -export const PipFocusLayout = memo(function PipFocusLayout({ - mainTrack, - thumbnailTrack, -}: PipFocusLayoutProps) { - return ( - - - - - {thumbnailTrack && ( - - - - )} - - ) -}) - -const FocusContainer = styled('div', { - base: { - position: 'relative', - width: '100%', - height: '100%', - borderRadius: '4px', - overflow: 'hidden', - backgroundColor: 'primaryDark.100', - }, -}) - -const MainSlot = styled('div', { - base: { - width: '100%', - height: '100%', - '& .lk-participant-media-video': { - objectFit: 'contain', - }, - }, -}) - -const Thumbnail = styled('div', { - base: { - position: 'absolute', - right: '1rem', - bottom: '1rem', - width: '42%', - maxWidth: '220px', - minWidth: '140px', - aspectRatio: '16 / 9', - borderRadius: '4px', - overflow: 'hidden', - boxShadow: 'md', - zIndex: 2, - }, -}) +import { memo } from 'react' +import type { TrackReferenceOrPlaceholder } from '@livekit/components-core' +import { styled } from '@/styled-system/jsx' +import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile' +import { getTrackKey } from '../../utils/pipTrackSelection' + +type PipFocusLayoutProps = { + mainTrack: TrackReferenceOrPlaceholder + thumbnailTrack?: TrackReferenceOrPlaceholder +} + +/** + * Focus layout used when 1-2 tracks are visible in the PiP window. + * + * The main tile is letterboxed (object-fit: contain) so the camera is + * never stretched to a non-video aspect and leaves dark padding + * above/below when the window shape doesn't match the source. + * The thumbnail keeps the usual cover fill. + */ +export const PipFocusLayout = memo(function PipFocusLayout({ + mainTrack, + thumbnailTrack, +}: PipFocusLayoutProps) { + return ( + + + + + {thumbnailTrack && ( + + + + )} + + ) +}) + +const FocusContainer = styled('div', { + base: { + position: 'relative', + width: '100%', + height: '100%', + borderRadius: '4px', + overflow: 'hidden', + backgroundColor: 'primaryDark.100', + }, +}) + +const MainSlot = styled('div', { + base: { + width: '100%', + height: '100%', + '& .lk-participant-media-video': { + objectFit: 'contain', + }, + }, +}) + +const Thumbnail = styled('div', { + base: { + position: 'absolute', + right: '1rem', + bottom: '1rem', + width: '42%', + maxWidth: '220px', + minWidth: '140px', + aspectRatio: '16 / 9', + borderRadius: '4px', + overflow: 'hidden', + boxShadow: 'md', + zIndex: 2, + }, +}) diff --git a/src/frontend/src/features/pip/components/layouts/PipGridLayout.tsx b/src/frontend/src/features/pip/components/layouts/PipGridLayout.tsx index 350f86cd..1c0080bb 100644 --- a/src/frontend/src/features/pip/components/layouts/PipGridLayout.tsx +++ b/src/frontend/src/features/pip/components/layouts/PipGridLayout.tsx @@ -1,82 +1,82 @@ -import { memo, useMemo, useRef } from 'react' -import type { TrackReferenceOrPlaceholder } from '@livekit/components-core' -import { styled } from '@/styled-system/jsx' -import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile' -import { usePipElementSize } from '../../hooks/usePipElementSize' -import { usePipFlipAnimations } from '../../hooks/usePipFlipAnimations' -import { computePipGridLayout } from '../../utils/pipGrid' -import { getTrackKey } from '../../utils/pipTrackSelection' - -type PipGridLayoutProps = { - tracks: TrackReferenceOrPlaceholder[] -} - -/** - * Adaptive grid used when 3+ tracks are visible in the PiP window. - * - * All grid math (shape choice + partial-row stretching) is delegated to - * `computePipGridLayout`. This component only measures the container, - * applies the returned placements, and plays a FLIP animation when the - * tile set or grid shape changes (participant joins/leaves or shape shift). - * - * Tiles keep a stable key so resizing never remounts