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