Skip to content

Commit d7fb31e

Browse files
committed
✨(frontend) show room notifications inside the PiP window
Render the shared toast queue in PiP.
1 parent c8ae87b commit d7fb31e

13 files changed

Lines changed: 540 additions & 256 deletions

File tree

src/frontend/src/features/notifications/MainNotificationToast.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect } from 'react'
1+
import { useCallback, useEffect, useRef } from 'react'
22
import { useRoomContext } from '@livekit/components-react'
33
import { Participant, RemoteParticipant, RoomEvent } from 'livekit-client'
44
import { ChatMessage, isMobileBrowser } from '@livekit/components-core'
@@ -24,12 +24,18 @@ export const MainNotificationToast = () => {
2424

2525
const { appendReaction } = useReactions()
2626

27+
// Chat uses keepAlive in multiple SidePanels (main + PiP), so the same
28+
// message event can fire more than once. Track the last id to deduplicate.
29+
const lastChatMsgIdRef = useRef<string>('')
30+
2731
useEffect(() => {
2832
const handleChatMessage = (
2933
chatMessage: ChatMessage,
3034
participant?: Participant | undefined
3135
) => {
3236
if (!participant || participant.isLocal) return
37+
if (chatMessage.id && chatMessage.id === lastChatMsgIdRef.current) return
38+
lastChatMsgIdRef.current = chatMessage.id ?? ''
3339
triggerNotificationSound(NotificationType.MessageReceived)
3440
toastQueue.add(
3541
{

src/frontend/src/features/pip/components/DocumentPiPPortal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export const DocumentPiPPortal = ({
170170
return () => {
171171
cancelled = true
172172
}
173-
}, [closePiP, isOpen, isSupported, openPiP])
173+
}, [closePiP, isOpen, isSupported, openPiP, t])
174174

175175
// Focus stays on the trigger; PiP is announced as an auxiliary surface.
176176
useEffect(() => {

src/frontend/src/features/pip/components/PipView.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { usePipRestoreFocus } from '../hooks/usePipRestoreFocus'
1111
import { PipControlBar } from './PipControlBar'
1212
import { PipReactionsToolbar } from './PipReactionsToolbar'
1313
import { PipStage } from './layouts/PipStage'
14+
import { PipNotificationOverlay } from './notifications/PipNotificationOverlay'
15+
import { PipConnectionStateToast } from './notifications/PipConnectionStateToast'
1416

1517
export const PipView = () => {
1618
const browserSupportsScreenSharing = supportsScreenSharing()
@@ -47,6 +49,8 @@ export const PipView = () => {
4749
<PipReactionsToolbar />
4850
<PipControlBar showScreenShare={browserSupportsScreenSharing} />
4951
<SidePanel store={pipLayoutStore} />
52+
<PipConnectionStateToast />
53+
<PipNotificationOverlay />
5054
</PipContainer>
5155
)
5256
}
Lines changed: 81 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,81 @@
1-
import { memo } from 'react'
2-
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
3-
import { styled } from '@/styled-system/jsx'
4-
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
5-
import { getTrackKey } from '../../utils/pipTrackSelection'
6-
7-
type PipFocusLayoutProps = {
8-
mainTrack: TrackReferenceOrPlaceholder
9-
thumbnailTrack?: TrackReferenceOrPlaceholder
10-
}
11-
12-
/**
13-
* Focus layout used when 1-2 tracks are visible in the PiP window.
14-
*
15-
* The main tile is letterboxed (object-fit: contain) so the camera is
16-
* never stretched to a non-video aspect and leaves dark padding
17-
* above/below when the window shape doesn't match the source.
18-
* The thumbnail keeps the usual cover fill.
19-
*/
20-
export const PipFocusLayout = memo(function PipFocusLayout({
21-
mainTrack,
22-
thumbnailTrack,
23-
}: PipFocusLayoutProps) {
24-
return (
25-
<FocusContainer>
26-
<MainSlot>
27-
<ParticipantTile
28-
key={getTrackKey(mainTrack)}
29-
trackRef={mainTrack}
30-
disableMetadata
31-
/>
32-
</MainSlot>
33-
{thumbnailTrack && (
34-
<Thumbnail>
35-
<ParticipantTile
36-
key={getTrackKey(thumbnailTrack)}
37-
trackRef={thumbnailTrack}
38-
disableMetadata
39-
/>
40-
</Thumbnail>
41-
)}
42-
</FocusContainer>
43-
)
44-
})
45-
46-
const FocusContainer = styled('div', {
47-
base: {
48-
position: 'relative',
49-
width: '100%',
50-
height: '100%',
51-
borderRadius: '4px',
52-
overflow: 'hidden',
53-
backgroundColor: 'primaryDark.100',
54-
},
55-
})
56-
57-
const MainSlot = styled('div', {
58-
base: {
59-
width: '100%',
60-
height: '100%',
61-
'& .lk-participant-media-video': {
62-
objectFit: 'contain',
63-
},
64-
},
65-
})
66-
67-
const Thumbnail = styled('div', {
68-
base: {
69-
position: 'absolute',
70-
right: '1rem',
71-
bottom: '1rem',
72-
width: '42%',
73-
maxWidth: '220px',
74-
minWidth: '140px',
75-
aspectRatio: '16 / 9',
76-
borderRadius: '4px',
77-
overflow: 'hidden',
78-
boxShadow: 'md',
79-
zIndex: 2,
80-
},
81-
})
1+
import { memo } from 'react'
2+
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
3+
import { styled } from '@/styled-system/jsx'
4+
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
5+
import { getTrackKey } from '../../utils/pipTrackSelection'
6+
7+
type PipFocusLayoutProps = {
8+
mainTrack: TrackReferenceOrPlaceholder
9+
thumbnailTrack?: TrackReferenceOrPlaceholder
10+
}
11+
12+
/**
13+
* Focus layout used when 1-2 tracks are visible in the PiP window.
14+
*
15+
* The main tile is letterboxed (object-fit: contain) so the camera is
16+
* never stretched to a non-video aspect and leaves dark padding
17+
* above/below when the window shape doesn't match the source.
18+
* The thumbnail keeps the usual cover fill.
19+
*/
20+
export const PipFocusLayout = memo(function PipFocusLayout({
21+
mainTrack,
22+
thumbnailTrack,
23+
}: PipFocusLayoutProps) {
24+
return (
25+
<FocusContainer>
26+
<MainSlot>
27+
<ParticipantTile
28+
key={getTrackKey(mainTrack)}
29+
trackRef={mainTrack}
30+
disableMetadata
31+
/>
32+
</MainSlot>
33+
{thumbnailTrack && (
34+
<Thumbnail>
35+
<ParticipantTile
36+
key={getTrackKey(thumbnailTrack)}
37+
trackRef={thumbnailTrack}
38+
disableMetadata
39+
/>
40+
</Thumbnail>
41+
)}
42+
</FocusContainer>
43+
)
44+
})
45+
46+
const FocusContainer = styled('div', {
47+
base: {
48+
position: 'relative',
49+
width: '100%',
50+
height: '100%',
51+
borderRadius: '4px',
52+
overflow: 'hidden',
53+
backgroundColor: 'primaryDark.100',
54+
},
55+
})
56+
57+
const MainSlot = styled('div', {
58+
base: {
59+
width: '100%',
60+
height: '100%',
61+
'& .lk-participant-media-video': {
62+
objectFit: 'contain',
63+
},
64+
},
65+
})
66+
67+
const Thumbnail = styled('div', {
68+
base: {
69+
position: 'absolute',
70+
right: '1rem',
71+
bottom: '1rem',
72+
width: '42%',
73+
maxWidth: '220px',
74+
minWidth: '140px',
75+
aspectRatio: '16 / 9',
76+
borderRadius: '4px',
77+
overflow: 'hidden',
78+
boxShadow: 'md',
79+
zIndex: 2,
80+
},
81+
})
Lines changed: 82 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,82 @@
1-
import { memo, useMemo, useRef } from 'react'
2-
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
3-
import { styled } from '@/styled-system/jsx'
4-
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
5-
import { usePipElementSize } from '../../hooks/usePipElementSize'
6-
import { usePipFlipAnimations } from '../../hooks/usePipFlipAnimations'
7-
import { computePipGridLayout } from '../../utils/pipGrid'
8-
import { getTrackKey } from '../../utils/pipTrackSelection'
9-
10-
type PipGridLayoutProps = {
11-
tracks: TrackReferenceOrPlaceholder[]
12-
}
13-
14-
/**
15-
* Adaptive grid used when 3+ tracks are visible in the PiP window.
16-
*
17-
* All grid math (shape choice + partial-row stretching) is delegated to
18-
* `computePipGridLayout`. This component only measures the container,
19-
* applies the returned placements, and plays a FLIP animation when the
20-
* tile set or grid shape changes (participant joins/leaves or shape shift).
21-
*
22-
* Tiles keep a stable key so resizing never remounts <video> elements.
23-
*/
24-
export const PipGridLayout = memo(function PipGridLayout({
25-
tracks,
26-
}: PipGridLayoutProps) {
27-
const containerRef = useRef<HTMLDivElement>(null)
28-
const { width, height } = usePipElementSize(containerRef)
29-
30-
const tileKeys = useMemo(() => tracks.map(getTrackKey), [tracks])
31-
32-
const { rows, subColumns, placements } = useMemo(
33-
() => computePipGridLayout(tracks.length, width, height),
34-
[tracks.length, width, height]
35-
)
36-
37-
const gridStyle = useMemo(
38-
() => ({
39-
gridTemplateColumns: `repeat(${subColumns}, minmax(0, 1fr))`,
40-
gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))`,
41-
}),
42-
[subColumns, rows]
43-
)
44-
45-
usePipFlipAnimations(containerRef, tileKeys)
46-
47-
return (
48-
<GridContainer ref={containerRef} style={gridStyle}>
49-
{tracks.map((track, index) => (
50-
<GridCell key={tileKeys[index]} style={placements[index]}>
51-
<ParticipantTile trackRef={track} disableMetadata />
52-
</GridCell>
53-
))}
54-
</GridContainer>
55-
)
56-
})
57-
58-
const GridContainer = styled('div', {
59-
base: {
60-
width: '100%',
61-
height: '100%',
62-
display: 'grid',
63-
gap: '0.25rem',
64-
},
65-
})
66-
67-
const GridCell = styled('div', {
68-
base: {
69-
position: 'relative',
70-
minWidth: 0,
71-
minHeight: 0,
72-
borderRadius: '4px',
73-
overflow: 'hidden',
74-
backgroundColor: 'primaryDark.100',
75-
// Paint on own layer so FLIP transforms don't trigger layout thrash.
76-
willChange: 'transform',
77-
'& .lk-participant-tile': {
78-
width: '100%',
79-
height: '100%',
80-
},
81-
},
82-
})
1+
import { memo, useMemo, useRef } from 'react'
2+
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
3+
import { styled } from '@/styled-system/jsx'
4+
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
5+
import { usePipElementSize } from '../../hooks/usePipElementSize'
6+
import { usePipFlipAnimations } from '../../hooks/usePipFlipAnimations'
7+
import { computePipGridLayout } from '../../utils/pipGrid'
8+
import { getTrackKey } from '../../utils/pipTrackSelection'
9+
10+
type PipGridLayoutProps = {
11+
tracks: TrackReferenceOrPlaceholder[]
12+
}
13+
14+
/**
15+
* Adaptive grid used when 3+ tracks are visible in the PiP window.
16+
*
17+
* All grid math (shape choice + partial-row stretching) is delegated to
18+
* `computePipGridLayout`. This component only measures the container,
19+
* applies the returned placements, and plays a FLIP animation when the
20+
* tile set or grid shape changes (participant joins/leaves or shape shift).
21+
*
22+
* Tiles keep a stable key so resizing never remounts <video> elements.
23+
*/
24+
export const PipGridLayout = memo(function PipGridLayout({
25+
tracks,
26+
}: PipGridLayoutProps) {
27+
const containerRef = useRef<HTMLDivElement>(null)
28+
const { width, height } = usePipElementSize(containerRef)
29+
30+
const tileKeys = useMemo(() => tracks.map(getTrackKey), [tracks])
31+
32+
const { rows, subColumns, placements } = useMemo(
33+
() => computePipGridLayout(tracks.length, width, height),
34+
[tracks.length, width, height]
35+
)
36+
37+
const gridStyle = useMemo(
38+
() => ({
39+
gridTemplateColumns: `repeat(${subColumns}, minmax(0, 1fr))`,
40+
gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))`,
41+
}),
42+
[subColumns, rows]
43+
)
44+
45+
usePipFlipAnimations(containerRef, tileKeys)
46+
47+
return (
48+
<GridContainer ref={containerRef} style={gridStyle}>
49+
{tracks.map((track, index) => (
50+
<GridCell key={tileKeys[index]} style={placements[index]}>
51+
<ParticipantTile trackRef={track} disableMetadata />
52+
</GridCell>
53+
))}
54+
</GridContainer>
55+
)
56+
})
57+
58+
const GridContainer = styled('div', {
59+
base: {
60+
width: '100%',
61+
height: '100%',
62+
display: 'grid',
63+
gap: '0.25rem',
64+
},
65+
})
66+
67+
const GridCell = styled('div', {
68+
base: {
69+
position: 'relative',
70+
minWidth: 0,
71+
minHeight: 0,
72+
borderRadius: '4px',
73+
overflow: 'hidden',
74+
backgroundColor: 'primaryDark.100',
75+
// Paint on own layer so FLIP transforms don't trigger layout thrash.
76+
willChange: 'transform',
77+
'& .lk-participant-tile': {
78+
width: '100%',
79+
height: '100%',
80+
},
81+
},
82+
})

0 commit comments

Comments
 (0)