♻️(frontend) normalize PiP component patterns

Extract OverflowItems, replace `any`, convert function declarations to const arrows.
This commit is contained in:
Cyril
2026-04-29 14:13:24 +02:00
parent 1080407bb3
commit 7e9b3a6290
5 changed files with 103 additions and 98 deletions
@@ -30,10 +30,10 @@ const COLLAPSE_ORDER: CollapsibleControl[] = [
const BUTTON_SLOT = 50
const ESSENTIAL_WIDTH = 260
function getHiddenControls(
const getHiddenControls = (
containerWidth: number,
showScreenShare: boolean
): Set<CollapsibleControl> {
): Set<CollapsibleControl> => {
const hidden = new Set<CollapsibleControl>()
if (containerWidth <= 0) return hidden
@@ -1,10 +1,4 @@
import { Menu as RACMenu, MenuItem, MenuSection } from 'react-aria-components'
import {
RiHand,
RiClosedCaptioningLine,
RiArrowUpLine,
RiEmotionLine,
} from '@remixicon/react'
import { Menu as RACMenu, MenuSection } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { Separator } from '@/primitives/Separator'
import { FeedbackMenuItem } from '@/features/rooms/livekit/components/controls/Options/FeedbackMenuItem'
@@ -12,12 +6,7 @@ import { EffectsMenuItem } from '@/features/rooms/livekit/components/controls/Op
import { SupportMenuItem } from '@/features/rooms/livekit/components/controls/Options/SupportMenuItem'
import { PictureInPictureMenuItem } from '@/features/rooms/livekit/components/controls/Options/PictureInPictureMenuItem'
import { pipLayoutStore } from '@/features/pip/stores/pipLayoutStore'
import { menuRecipe } from '@/primitives/menuRecipe'
import { useRoomContext } from '@livekit/components-react'
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
import { useSubtitles } from '@/features/subtitle/hooks/useSubtitles'
import { useAreSubtitlesAvailable } from '@/features/subtitle/hooks/useAreSubtitlesAvailable'
import { useSnapshot } from 'valtio'
import { PipOverflowItems } from './PipOverflowItems'
import type { CollapsibleControl } from '../PipControlBar'
type PipOptionsMenuItemsProps = {
@@ -40,7 +29,7 @@ export const PipOptionsMenuItems = ({
{hasOverflow && (
<>
<MenuSection>
<OverflowItems overflowControls={overflowControls} t={t} />
<PipOverflowItems overflowControls={overflowControls} t={t} />
</MenuSection>
<Separator />
</>
@@ -57,60 +46,3 @@ export const PipOptionsMenuItems = ({
</RACMenu>
)
}
const OverflowItems = ({
overflowControls,
t,
}: {
overflowControls: Set<CollapsibleControl>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
t: any
}) => {
const room = useRoomContext()
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
participant: room.localParticipant,
})
const { areSubtitlesOpen, toggleSubtitles } = useSubtitles()
const areSubtitlesAvailable = useAreSubtitlesAvailable()
const pipSnap = useSnapshot(pipLayoutStore)
const toggleReactions = () => {
pipLayoutStore.showReactionsToolbar = !pipSnap.showReactionsToolbar
}
const itemClass = menuRecipe({ icon: true, variant: 'dark' }).item
return (
<>
{overflowControls.has('reactions') && (
<MenuItem onAction={toggleReactions} className={itemClass}>
<RiEmotionLine size={20} />
{t('controls.reactions.button')}
</MenuItem>
)}
{overflowControls.has('screenShare') && (
<MenuItem
onAction={() => {
/* screen share requires track toggle, handled externally */
}}
className={itemClass}
>
<RiArrowUpLine size={20} />
{t('controls.screenShare.start')}
</MenuItem>
)}
{overflowControls.has('subtitles') && areSubtitlesAvailable && (
<MenuItem onAction={toggleSubtitles} className={itemClass}>
<RiClosedCaptioningLine size={20} />
{areSubtitlesOpen
? t('controls.subtitles.open')
: t('controls.subtitles.closed')}
</MenuItem>
)}
{overflowControls.has('hand') && (
<MenuItem onAction={toggleRaisedHand} className={itemClass}>
<RiHand size={20} />
{isHandRaised ? t('controls.hand.lower') : t('controls.hand.raise')}
</MenuItem>
)}
</>
)
}
@@ -0,0 +1,74 @@
import { MenuItem } from 'react-aria-components'
import {
RiHand,
RiClosedCaptioningLine,
RiArrowUpLine,
RiEmotionLine,
} from '@remixicon/react'
import { TFunction } from 'i18next'
import { pipLayoutStore } from '@/features/pip/stores/pipLayoutStore'
import { menuRecipe } from '@/primitives/menuRecipe'
import { useRoomContext } from '@livekit/components-react'
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
import { useSubtitles } from '@/features/subtitle/hooks/useSubtitles'
import { useAreSubtitlesAvailable } from '@/features/subtitle/hooks/useAreSubtitlesAvailable'
import { useSnapshot } from 'valtio'
import type { CollapsibleControl } from '../PipControlBar'
type PipOverflowItemsProps = {
overflowControls: Set<CollapsibleControl>
t: TFunction<'rooms'>
}
export const PipOverflowItems = ({
overflowControls,
t,
}: PipOverflowItemsProps) => {
const room = useRoomContext()
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
participant: room.localParticipant,
})
const { areSubtitlesOpen, toggleSubtitles } = useSubtitles()
const areSubtitlesAvailable = useAreSubtitlesAvailable()
const pipSnap = useSnapshot(pipLayoutStore)
const toggleReactions = () => {
pipLayoutStore.showReactionsToolbar = !pipSnap.showReactionsToolbar
}
const itemClass = menuRecipe({ icon: true, variant: 'dark' }).item
return (
<>
{overflowControls.has('reactions') && (
<MenuItem onAction={toggleReactions} className={itemClass}>
<RiEmotionLine size={20} />
{t('controls.reactions.button')}
</MenuItem>
)}
{overflowControls.has('screenShare') && (
<MenuItem
onAction={() => {
/* screen share requires track toggle, handled externally */
}}
className={itemClass}
>
<RiArrowUpLine size={20} />
{t('controls.screenShare.start')}
</MenuItem>
)}
{overflowControls.has('subtitles') && areSubtitlesAvailable && (
<MenuItem onAction={toggleSubtitles} className={itemClass}>
<RiClosedCaptioningLine size={20} />
{areSubtitlesOpen
? t('controls.subtitles.open')
: t('controls.subtitles.closed')}
</MenuItem>
)}
{overflowControls.has('hand') && (
<MenuItem onAction={toggleRaisedHand} className={itemClass}>
<RiHand size={20} />
{isHandRaised ? t('controls.hand.lower') : t('controls.hand.raise')}
</MenuItem>
)}
</>
)
}
@@ -17,31 +17,31 @@ type PipFocusLayoutProps = {
* 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 (
<FocusContainer>
<MainSlot>
<ParticipantTile
key={getTrackKey(mainTrack)}
trackRef={mainTrack}
disableMetadata
/>
</MainSlot>
{thumbnailTrack && (
<Thumbnail>
export const PipFocusLayout = memo(
({ mainTrack, thumbnailTrack }: PipFocusLayoutProps) => {
return (
<FocusContainer>
<MainSlot>
<ParticipantTile
key={getTrackKey(thumbnailTrack)}
trackRef={thumbnailTrack}
key={getTrackKey(mainTrack)}
trackRef={mainTrack}
disableMetadata
/>
</Thumbnail>
)}
</FocusContainer>
)
})
</MainSlot>
{thumbnailTrack && (
<Thumbnail>
<ParticipantTile
key={getTrackKey(thumbnailTrack)}
trackRef={thumbnailTrack}
disableMetadata
/>
</Thumbnail>
)}
</FocusContainer>
)
}
)
PipFocusLayout.displayName = 'PipFocusLayout'
const FocusContainer = styled('div', {
base: {
@@ -21,9 +21,7 @@ type PipGridLayoutProps = {
*
* Tiles keep a stable key so resizing never remounts <video> elements.
*/
export const PipGridLayout = memo(function PipGridLayout({
tracks,
}: PipGridLayoutProps) {
export const PipGridLayout = memo(({ tracks }: PipGridLayoutProps) => {
const containerRef = useRef<HTMLDivElement>(null)
const { width, height } = usePipElementSize(containerRef)
@@ -54,6 +52,7 @@ export const PipGridLayout = memo(function PipGridLayout({
</GridContainer>
)
})
PipGridLayout.displayName = 'PipGridLayout'
const GridContainer = styled('div', {
base: {