♻️(refactor) inject side panel store via context for PiP

Keep pipLayoutStore separate from layoutStore; shared components use context.
This commit is contained in:
Cyril
2026-05-07 11:50:47 +02:00
parent 5a508aeb31
commit 3d81110f56
9 changed files with 34 additions and 27 deletions
@@ -3,7 +3,10 @@ import { supportsScreenSharing } from '@livekit/components-core'
import { useTranslation } from 'react-i18next'
import { styled } from '@/styled-system/jsx'
import { SidePanel } from '@/features/rooms/livekit/components/SidePanel'
import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
import {
SidePanelStoreProvider,
useSidePanel,
} from '@/features/rooms/livekit/hooks/useSidePanel'
import { pipLayoutStore } from '../stores/pipLayoutStore'
import { useEscapeDismiss } from '../hooks/useEscapeDismiss'
import { usePipKeyboardShortcuts } from '../hooks/usePipKeyboardShortcuts'
@@ -17,12 +20,20 @@ import { PipConnectionStateToast } from './notifications/PipConnectionStateToast
import { PipReactionPortals } from './PipReactionPortals'
export const PipView = () => {
return (
<SidePanelStoreProvider value={pipLayoutStore}>
<PipViewContent />
</SidePanelStoreProvider>
)
}
const PipViewContent = () => {
const browserSupportsScreenSharing = supportsScreenSharing()
const { t } = useTranslation('rooms', {
keyPrefix: 'options.items.pictureInPicture',
})
const containerRef = useRef<HTMLDivElement>(null)
const { isSidePanelOpen, closePanel } = useSidePanel(pipLayoutStore)
const { isSidePanelOpen, closePanel } = useSidePanel()
// Escape closes the side panel instead of the whole PiP window.
useEscapeDismiss(containerRef, isSidePanelOpen, closePanel)
@@ -53,7 +64,7 @@ export const PipView = () => {
<PipStage />
<PipReactionsToolbar />
<PipControlBar showScreenShare={browserSupportsScreenSharing} />
<SidePanel store={pipLayoutStore} />
<SidePanel />
<PipReactionPortals />
<OverlayStack>
<PipConnectionStateToast />
@@ -4,7 +4,6 @@ import { FeedbackMenuItem } from '@/features/rooms/livekit/components/controls/O
import { EffectsMenuItem } from '@/features/rooms/livekit/components/controls/Options/EffectsMenuItem'
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 { PipOverflowItems } from './PipOverflowItems'
import type { CollapsibleControl } from '../PipControlBar'
@@ -34,7 +33,7 @@ export const PipOptionsMenuItems = ({
)}
<MenuSection>
<PictureInPictureMenuItem />
<EffectsMenuItem store={pipLayoutStore} />
<EffectsMenuItem />
</MenuSection>
<Separator />
<MenuSection>
@@ -2,7 +2,6 @@ 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 { pipLayoutStore } from '../../stores/pipLayoutStore'
import { getTrackKey } from '../../utils/pipTrackSelection'
type PipFocusLayoutProps = {
@@ -26,7 +25,6 @@ export const PipFocusLayout = memo(
<ParticipantTile
key={getTrackKey(mainTrack)}
trackRef={mainTrack}
sidePanelStore={pipLayoutStore}
/>
</MainSlot>
{thumbnailTrack && (
@@ -34,7 +32,6 @@ export const PipFocusLayout = memo(
<ParticipantTile
key={getTrackKey(thumbnailTrack)}
trackRef={thumbnailTrack}
sidePanelStore={pipLayoutStore}
/>
</Thumbnail>
)}
@@ -2,7 +2,6 @@ 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 { pipLayoutStore } from '../../stores/pipLayoutStore'
import { usePipElementSize } from '../../hooks/usePipElementSize'
import { usePipFlipAnimations } from '../../hooks/usePipFlipAnimations'
import { computePipGridLayout } from '../../utils/pipGrid'
@@ -47,7 +46,7 @@ export const PipGridLayout = memo(({ tracks }: PipGridLayoutProps) => {
<GridContainer ref={containerRef} style={gridStyle}>
{tracks.map((track, index) => (
<GridCell key={tileKeys[index]} style={placements[index]}>
<ParticipantTile trackRef={track} sidePanelStore={pipLayoutStore} />
<ParticipantTile trackRef={track} />
</GridCell>
))}
</GridContainer>
@@ -27,7 +27,6 @@ import { HStack } from '@/styled-system/jsx'
import { MutedMicIndicator } from './MutedMicIndicator'
import { ParticipantPlaceholder } from './ParticipantPlaceholder'
import { ParticipantTileFocus } from './ParticipantTileFocus'
import type { SidePanelStore } from '../hooks/useSidePanel'
import { FullScreenShareWarning } from './FullScreenShareWarning'
import { ParticipantName } from './ParticipantName'
import { getParticipantName } from '@/features/rooms/utils/getParticipantName'
@@ -53,7 +52,6 @@ export function TrackRefContextIfNeeded(
interface ParticipantTileExtendedProps extends ParticipantTileProps {
disableMetadata?: boolean
sidePanelStore?: SidePanelStore
}
export const ParticipantTile: (
@@ -68,7 +66,6 @@ export const ParticipantTile: (
onParticipantClick,
disableSpeakingIndicator,
disableMetadata,
sidePanelStore,
...htmlProps
}: ParticipantTileExtendedProps,
ref
@@ -238,7 +235,6 @@ export const ParticipantTile: (
<ParticipantTileFocus
trackRef={trackReference}
hasKeyboardFocus={hasKeyboardFocus}
sidePanelStore={sidePanelStore}
/>
)}
</ParticipantContextIfNeeded>
@@ -16,7 +16,7 @@ import {
import { useTranslation } from 'react-i18next'
import { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import { useEffect, useRef, useState } from 'react'
import { type SidePanelStore, useSidePanel } from '../hooks/useSidePanel'
import { useSidePanel } from '../hooks/useSidePanel'
import { useFullScreen } from '../hooks/useFullScreen'
import { Participant, Track } from 'livekit-client'
import { MuteAlertDialog } from './MuteAlertDialog'
@@ -74,9 +74,9 @@ const FocusButton = ({
)
}
const EffectsButton = ({ store }: { store?: SidePanelStore }) => {
const EffectsButton = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
const { isEffectsOpen, toggleEffects } = useSidePanel(store)
const { isEffectsOpen, toggleEffects } = useSidePanel()
return (
<Button
size={'sm'}
@@ -132,11 +132,9 @@ const MOUSE_IDLE_TIME = 3000
export const ParticipantTileFocus = ({
trackRef,
hasKeyboardFocus,
sidePanelStore,
}: {
trackRef: TrackReferenceOrPlaceholder
hasKeyboardFocus: boolean
sidePanelStore?: SidePanelStore
}) => {
const [hovered, setHovered] = useState(false)
const [opacity, setOpacity] = useState(0)
@@ -218,7 +216,7 @@ export const ParticipantTileFocus = ({
{!isScreenShare ? (
<>
{participant.isLocal ? (
<EffectsButton store={sidePanelStore} />
<EffectsButton />
) : (
canMute && <MuteButton participant={participant} />
)}
@@ -5,7 +5,7 @@ import { Button, Div } from '@/primitives'
import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList'
import { type SidePanelStore, useSidePanel } from '../hooks/useSidePanel'
import { useSidePanel } from '../hooks/useSidePanel'
import { ReactNode } from 'react'
import { Chat } from '../prefabs/Chat'
import { Effects } from './effects/Effects'
@@ -143,7 +143,7 @@ const Panel = ({ isOpen, keepAlive = false, children }: PanelProps) => (
{keepAlive || isOpen ? children : null}
</div>
)
export const SidePanel = ({ store }: { store?: SidePanelStore }) => {
export const SidePanel = () => {
const {
activePanelId,
isParticipantsOpen,
@@ -157,7 +157,7 @@ export const SidePanel = ({ store }: { store?: SidePanelStore }) => {
activeSubPanelId,
closePanel,
goBack,
} = useSidePanel(store)
} = useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
const title = t(`heading.${activeSubPanelId || activePanelId}`)
@@ -2,11 +2,11 @@ import { RiImageCircleAiFill } from '@remixicon/react'
import { MenuItem } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { menuRecipe } from '@/primitives/menuRecipe'
import { type SidePanelStore, useSidePanel } from '../../../hooks/useSidePanel'
import { useSidePanel } from '../../../hooks/useSidePanel'
export const EffectsMenuItem = ({ store }: { store?: SidePanelStore }) => {
export const EffectsMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const { toggleEffects } = useSidePanel(store)
const { toggleEffects } = useSidePanel()
return (
<MenuItem
@@ -1,3 +1,4 @@
import { createContext, useContext } from 'react'
import { useSnapshot } from 'valtio'
import { layoutStore } from '@/stores/layout'
import { PanelId, SubPanelId } from '../types/panel'
@@ -9,7 +10,13 @@ export type SidePanelStore = {
activeSubPanelId: SubPanelId | null
}
export const useSidePanel = (store: SidePanelStore = layoutStore) => {
const SidePanelStoreContext = createContext<SidePanelStore>(layoutStore)
export const SidePanelStoreProvider = SidePanelStoreContext.Provider
export const useSidePanel = (store?: SidePanelStore) => {
const currentStore = useContext(SidePanelStoreContext)
store ??= currentStore
const layoutSnap = useSnapshot(store)
const activePanelId = layoutSnap.activePanelId
const activeSubPanelId = layoutSnap.activeSubPanelId