mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-26 11:58:53 +00:00
♻️(frontend) vendor pinned track context using a Valtio store
Vendor the pinned track context and its associated hook from LiveKit, and switch the underlying state management to a Valtio store. The LiveKit ContextProvider was injected high in the tree, so any pinned track change triggered a re-render of a large portion of the app. With a Valtio store, only the parts of the app that actually subscribe to the pinned track or need to pin one re-render when present in the DOM.
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
FocusLayoutContainer,
|
||||
useMaybeLayoutContext,
|
||||
usePinnedTracks,
|
||||
useRoomContext,
|
||||
useTracks,
|
||||
} from '@livekit/components-react'
|
||||
@@ -16,21 +14,19 @@ import {
|
||||
type TrackReferenceOrPlaceholder,
|
||||
} from '@livekit/components-core'
|
||||
import { type Participant, RoomEvent, Track } from 'livekit-client'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { clearPinnedTrack, layoutStore, setPinnedTrack } from '@/stores/layout'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getParticipantName } from '@/features/rooms/utils/getParticipantName'
|
||||
|
||||
|
||||
export const StageLayout = () => {
|
||||
|
||||
const lastAutoFocusedScreenShareTrack =
|
||||
useRef<TrackReferenceOrPlaceholder | null>(null)
|
||||
|
||||
const lastPinnedParticipantIdentityRef = useRef<string | null>(null)
|
||||
|
||||
const layoutContext = useMaybeLayoutContext()
|
||||
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'pinAnnouncements' })
|
||||
const { t: tRooms } = useTranslation('rooms')
|
||||
|
||||
@@ -59,15 +55,15 @@ export const StageLayout = () => {
|
||||
.filter(isTrackReference)
|
||||
.filter((track) => track.publication.source === Track.Source.ScreenShare)
|
||||
|
||||
const focusTrack = usePinnedTracks(layoutContext)?.[0]
|
||||
const { pinnedTrackRef } = useSnapshot(layoutStore)
|
||||
|
||||
const carouselTracks = tracks.filter(
|
||||
(track) => !isEqualTrackRef(track, focusTrack)
|
||||
(track) => !isEqualTrackRef(track, pinnedTrackRef)
|
||||
)
|
||||
|
||||
// // handle pin announcements
|
||||
|
||||
// handle pin announcements
|
||||
useEffect(() => {
|
||||
const participant = focusTrack?.participant
|
||||
const participant = pinnedTrackRef?.participant
|
||||
|
||||
// 1. unpin
|
||||
if (!participant) {
|
||||
@@ -109,7 +105,7 @@ export const StageLayout = () => {
|
||||
)
|
||||
}, [
|
||||
announce,
|
||||
focusTrack,
|
||||
pinnedTrackRef,
|
||||
getAnnouncementName,
|
||||
room.localParticipant,
|
||||
room.remoteParticipants,
|
||||
@@ -120,9 +116,6 @@ export const StageLayout = () => {
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
// Code duplicated from LiveKit; this warning will be addressed in the refactoring.
|
||||
useEffect(() => {
|
||||
|
||||
if (!layoutContext) return
|
||||
|
||||
// If screen share tracks are published, and no pin is set explicitly, auto set the screen share.
|
||||
if (
|
||||
screenShareTracks.some((track) => track.publication.isSubscribed) &&
|
||||
@@ -131,10 +124,7 @@ export const StageLayout = () => {
|
||||
log.debug('Auto set screen share focus:', {
|
||||
newScreenShareTrack: screenShareTracks[0],
|
||||
})
|
||||
layoutContext.pin.dispatch?.({
|
||||
msg: 'set_pin',
|
||||
trackReference: screenShareTracks[0],
|
||||
})
|
||||
setPinnedTrack(screenShareTracks[0])
|
||||
lastAutoFocusedScreenShareTrack.current = screenShareTracks[0]
|
||||
} else if (
|
||||
lastAutoFocusedScreenShareTrack.current &&
|
||||
@@ -145,23 +135,20 @@ export const StageLayout = () => {
|
||||
)
|
||||
) {
|
||||
log.debug('Auto clearing screen share focus.')
|
||||
layoutContext.pin.dispatch?.({ msg: 'clear_pin' })
|
||||
clearPinnedTrack()
|
||||
lastAutoFocusedScreenShareTrack.current = null
|
||||
}
|
||||
if (focusTrack && !isTrackReference(focusTrack)) {
|
||||
if (pinnedTrackRef && !isTrackReference(pinnedTrackRef)) {
|
||||
const updatedFocusTrack = tracks.find(
|
||||
(tr) =>
|
||||
tr.participant.identity === focusTrack.participant.identity &&
|
||||
tr.source === focusTrack.source
|
||||
tr.participant.identity === pinnedTrackRef.participant.identity &&
|
||||
tr.source === pinnedTrackRef.source
|
||||
)
|
||||
if (
|
||||
updatedFocusTrack !== focusTrack &&
|
||||
updatedFocusTrack !== pinnedTrackRef &&
|
||||
isTrackReference(updatedFocusTrack)
|
||||
) {
|
||||
layoutContext.pin.dispatch?.({
|
||||
msg: 'set_pin',
|
||||
trackReference: updatedFocusTrack,
|
||||
})
|
||||
setPinnedTrack(updatedFocusTrack)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
@@ -170,15 +157,14 @@ export const StageLayout = () => {
|
||||
(ref) => `${ref.publication.trackSid}_${ref.publication.isSubscribed}`
|
||||
)
|
||||
.join(),
|
||||
focusTrack?.publication?.trackSid,
|
||||
pinnedTrackRef?.publication?.trackSid,
|
||||
tracks,
|
||||
])
|
||||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{!focusTrack ? (
|
||||
{!pinnedTrackRef ? (
|
||||
<div className="lk-grid-layout-wrapper" style={{ height: 'auto' }}>
|
||||
<GridLayout tracks={tracks} style={{ padding: 0 }}>
|
||||
<ParticipantTile />
|
||||
@@ -195,10 +181,10 @@ export const StageLayout = () => {
|
||||
>
|
||||
<ParticipantTile />
|
||||
</CarouselLayout>
|
||||
{focusTrack && <FocusLayout trackRef={focusTrack} />}
|
||||
{pinnedTrackRef && <FocusLayout trackRef={pinnedTrackRef} />}
|
||||
</FocusLayoutContainer>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import { Button as RACButton } from 'react-aria-components'
|
||||
import { Track } from 'livekit-client'
|
||||
import Source = Track.Source
|
||||
|
||||
import { useMaybeLayoutContext } from '@livekit/components-react'
|
||||
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
|
||||
import { type ToastProps } from './Toast'
|
||||
import { HStack, styled } from '@/styled-system/jsx'
|
||||
import { Div } from '@/primitives'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyledToastContainer } from './StyledToastContainer'
|
||||
import { setPinnedTrack } from '@/stores/layout'
|
||||
|
||||
const ClickableToast = styled(RACButton, {
|
||||
base: {
|
||||
@@ -28,7 +28,6 @@ export function ToastJoined({ state, ...props }: Readonly<ToastProps>) {
|
||||
state,
|
||||
ref
|
||||
)
|
||||
const layoutContext = useMaybeLayoutContext()
|
||||
const participant = props.toast.content.participant
|
||||
|
||||
if (!participant) return
|
||||
@@ -38,18 +37,13 @@ export function ToastJoined({ state, ...props }: Readonly<ToastProps>) {
|
||||
publication: participant.getTrackPublication(Source.Camera),
|
||||
source: Source.Camera,
|
||||
}
|
||||
const pinParticipant = () => {
|
||||
layoutContext?.pin.dispatch?.({
|
||||
msg: 'set_pin',
|
||||
trackReference,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledToastContainer {...toastProps} ref={ref}>
|
||||
<ClickableToast
|
||||
ref={ref}
|
||||
onPress={(e) => {
|
||||
pinParticipant()
|
||||
setPinnedTrack(trackReference)
|
||||
closeButtonProps.onPress?.(e)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
import type { Participant } from 'livekit-client'
|
||||
import { Participant, Track } from 'livekit-client'
|
||||
import { menuRecipe } from '@/primitives/menuRecipe'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { RiPushpin2Line, RiUnpinLine } from '@remixicon/react'
|
||||
import { MenuItem } from 'react-aria-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useFocusToggleParticipant } from '@/features/rooms/livekit/hooks/useFocusToggleParticipant'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { clearPinnedTrack, layoutStore, setPinnedTrack } from '@/stores/layout'
|
||||
import { isEqualTrackRef } from '@livekit/components-core'
|
||||
import Source = Track.Source
|
||||
import { useMemo } from 'react'
|
||||
|
||||
export const PinMenuItem = ({ participant }: { participant: Participant }) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'participantMenu' })
|
||||
|
||||
const { toggle, inFocus } = useFocusToggleParticipant(participant)
|
||||
const trackRef = useMemo(() => {
|
||||
return {
|
||||
participant: participant,
|
||||
publication: participant.getTrackPublication(Source.Camera),
|
||||
source: Source.Camera,
|
||||
}
|
||||
}, [participant])
|
||||
|
||||
const { pinnedTrackRef } = useSnapshot(layoutStore)
|
||||
const inFocus = isEqualTrackRef(pinnedTrackRef, trackRef)
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
@@ -17,7 +30,7 @@ export const PinMenuItem = ({ participant }: { participant: Participant }) => {
|
||||
name: participant.name,
|
||||
})}
|
||||
className={menuRecipe({ icon: true }).item}
|
||||
onAction={toggle}
|
||||
onAction={() => (inFocus ? clearPinnedTrack() : setPinnedTrack(trackRef))}
|
||||
>
|
||||
<HStack gap={0.25}>
|
||||
{inFocus ? (
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
useEnsureTrackRef,
|
||||
useFeatureContext,
|
||||
useIsEncrypted,
|
||||
useMaybeLayoutContext,
|
||||
useMaybeTrackRefContext,
|
||||
useParticipantTile,
|
||||
VideoTrack,
|
||||
@@ -16,8 +15,8 @@ import {
|
||||
} from '@livekit/components-react'
|
||||
import React from 'react'
|
||||
import {
|
||||
isEqualTrackRef,
|
||||
isTrackReference,
|
||||
isTrackReferencePinned,
|
||||
TrackReferenceOrPlaceholder,
|
||||
} from '@livekit/components-core'
|
||||
import { Track } from 'livekit-client'
|
||||
@@ -34,6 +33,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { getShortcutDescriptorById } from '@/features/shortcuts/catalog'
|
||||
import { formatShortcutLabel } from '@/features/shortcuts/formatLabels'
|
||||
import { KeyboardShortcutHint } from './KeyboardShortcutHint'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { layoutStore, clearPinnedTrack } from '@/stores/layout'
|
||||
|
||||
export function TrackRefContextIfNeeded(
|
||||
props: React.PropsWithChildren<{
|
||||
@@ -72,6 +73,8 @@ export const ParticipantTile: (
|
||||
}: ParticipantTileExtendedProps,
|
||||
ref
|
||||
) {
|
||||
const { pinnedTrackRef } = useSnapshot(layoutStore)
|
||||
|
||||
const trackReference = useEnsureTrackRef(trackRef)
|
||||
|
||||
const { elementProps } = useParticipantTile<HTMLDivElement>({
|
||||
@@ -81,8 +84,6 @@ export const ParticipantTile: (
|
||||
trackRef: trackReference,
|
||||
})
|
||||
const isEncrypted = useIsEncrypted(trackReference.participant)
|
||||
const layoutContext = useMaybeLayoutContext()
|
||||
|
||||
const autoManageSubscription = useFeatureContext()?.autoSubscription
|
||||
|
||||
const handleSubscribe = React.useCallback(
|
||||
@@ -90,14 +91,13 @@ export const ParticipantTile: (
|
||||
if (
|
||||
trackReference.source &&
|
||||
!subscribed &&
|
||||
layoutContext &&
|
||||
layoutContext.pin.dispatch &&
|
||||
isTrackReferencePinned(trackReference, layoutContext.pin.state)
|
||||
pinnedTrackRef &&
|
||||
isEqualTrackRef(trackReference, pinnedTrackRef)
|
||||
) {
|
||||
layoutContext.pin.dispatch({ msg: 'clear_pin' })
|
||||
clearPinnedTrack()
|
||||
}
|
||||
},
|
||||
[trackReference, layoutContext]
|
||||
[trackReference, pinnedTrackRef]
|
||||
)
|
||||
|
||||
const { isHandRaised } = useRaisedHand({
|
||||
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
RiPushpin2Line,
|
||||
RiUnpinLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
useFocusToggle,
|
||||
useTrackMutedIndicator,
|
||||
} from '@livekit/components-react'
|
||||
import { useTrackMutedIndicator } from '@livekit/components-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||
import {
|
||||
isEqualTrackRef,
|
||||
TrackReferenceOrPlaceholder,
|
||||
} from '@livekit/components-core'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useSidePanel } from '../hooks/useSidePanel'
|
||||
import { useFullScreen } from '../hooks/useFullScreen'
|
||||
@@ -22,6 +22,8 @@ import { type Participant, Track } from 'livekit-client'
|
||||
import { MuteAlertDialog } from './MuteAlertDialog'
|
||||
import { useMuteParticipant } from '@/features/rooms/api/muteParticipant'
|
||||
import { useCanMute } from '@/features/rooms/livekit/hooks/useCanMute'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { layoutStore, setPinnedTrack, clearPinnedTrack } from '@/stores/layout'
|
||||
|
||||
const ZoomButton = ({
|
||||
trackRef,
|
||||
@@ -56,18 +58,17 @@ const FocusButton = ({
|
||||
trackRef: TrackReferenceOrPlaceholder
|
||||
}) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
|
||||
const { mergedProps, inFocus } = useFocusToggle({
|
||||
trackRef,
|
||||
props: {},
|
||||
})
|
||||
|
||||
const { pinnedTrackRef } = useSnapshot(layoutStore)
|
||||
const inFocus = isEqualTrackRef(trackRef, pinnedTrackRef)
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primaryTextDark"
|
||||
square
|
||||
tooltip={inFocus ? t('pin.disable') : t('pin.enable')}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onPress={(event) => mergedProps?.onClick?.(event as any)}
|
||||
onPress={() => (inFocus ? clearPinnedTrack() : setPinnedTrack(trackRef))}
|
||||
>
|
||||
{inFocus ? <RiUnpinLine /> : <RiPushpin2Line />}
|
||||
</Button>
|
||||
|
||||
+26
-3
@@ -1,10 +1,33 @@
|
||||
import { useFocusToggleParticipant } from '@/features/rooms/livekit/hooks/useFocusToggleParticipant'
|
||||
import type { Participant } from 'livekit-client'
|
||||
import { Participant, Track } from 'livekit-client'
|
||||
import { RiPushpin2Fill } from '@remixicon/react'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useMemo } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { layoutStore } from '@/stores/layout'
|
||||
import { isEqualTrackRef } from '@livekit/components-core'
|
||||
import Source = Track.Source
|
||||
|
||||
export const PinBadge = ({ participant }: { participant: Participant }) => {
|
||||
const { inFocus } = useFocusToggleParticipant(participant)
|
||||
const cameraTrackRef = useMemo(() => {
|
||||
return {
|
||||
participant: participant,
|
||||
publication: participant.getTrackPublication(Source.Camera),
|
||||
source: Source.Camera,
|
||||
}
|
||||
}, [participant])
|
||||
|
||||
const screenShareTrackRef = useMemo(() => {
|
||||
return {
|
||||
participant: participant,
|
||||
publication: participant.getTrackPublication(Source.ScreenShare),
|
||||
source: Source.ScreenShare,
|
||||
}
|
||||
}, [participant])
|
||||
|
||||
const { pinnedTrackRef } = useSnapshot(layoutStore)
|
||||
const inFocus =
|
||||
isEqualTrackRef(pinnedTrackRef, cameraTrackRef) ||
|
||||
isEqualTrackRef(pinnedTrackRef, screenShareTrackRef)
|
||||
|
||||
if (!inFocus) return
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
|
||||
import {
|
||||
isWeb,
|
||||
} from '@livekit/components-core'
|
||||
import { isWeb } from '@livekit/components-core'
|
||||
import { Track } from 'livekit-client'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import {
|
||||
ConnectionStateToast,
|
||||
LayoutContextProvider,
|
||||
RoomAudioRenderer,
|
||||
useCreateLayoutContext,
|
||||
} from '@livekit/components-react'
|
||||
|
||||
import { ControlBar } from './ControlBar/ControlBar'
|
||||
@@ -58,7 +53,6 @@ export interface VideoConferenceProps extends React.HTMLAttributes<HTMLDivElemen
|
||||
* @public
|
||||
*/
|
||||
export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
|
||||
const { toggleSettingsDialog } = useSettingsDialog()
|
||||
|
||||
useConnectionObserver()
|
||||
@@ -73,8 +67,6 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
}, [toggleSettingsDialog]),
|
||||
})
|
||||
|
||||
const layoutContext = useCreateLayoutContext()
|
||||
|
||||
useNoiseReduction()
|
||||
|
||||
const { isOpen: isPictureInPictureOpen } = usePictureInPicture()
|
||||
@@ -90,21 +82,14 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
}}
|
||||
>
|
||||
{isWeb() && (
|
||||
<LayoutContextProvider
|
||||
value={layoutContext}
|
||||
// onPinChange={handleFocusStateChange}
|
||||
>
|
||||
<>
|
||||
<ScreenShareErrorModal
|
||||
isOpen={isShareErrorVisible}
|
||||
onClose={() => setIsShareErrorVisible(false)}
|
||||
/>
|
||||
<IsIdleDisconnectModal />
|
||||
<RoomContentArea>
|
||||
{isPictureInPictureOpen ? (
|
||||
<PipRoomPlaceholder />
|
||||
) : (
|
||||
<StageLayout />
|
||||
)}
|
||||
{isPictureInPictureOpen ? <PipRoomPlaceholder /> : <StageLayout />}
|
||||
</RoomContentArea>
|
||||
<ControlBar
|
||||
onDeviceError={(e) => {
|
||||
@@ -119,7 +104,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
}}
|
||||
/>
|
||||
<SidePanel />
|
||||
</LayoutContextProvider>
|
||||
</>
|
||||
)}
|
||||
<RoomAudioRenderer />
|
||||
<ConnectionStateToast />
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { proxy } from 'valtio'
|
||||
import { proxy, ref } from 'valtio'
|
||||
import type {
|
||||
PanelId,
|
||||
SubPanelId,
|
||||
} from '@/features/rooms/livekit/hooks/useSidePanel'
|
||||
import { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||
|
||||
type PinnedTrackRef = ReturnType<typeof ref<TrackReferenceOrPlaceholder>>
|
||||
|
||||
type State = {
|
||||
showHeader: boolean
|
||||
@@ -11,6 +14,7 @@ type State = {
|
||||
activePanelId: PanelId | null
|
||||
activeSubPanelId: SubPanelId | null
|
||||
showReactionsToolbar: boolean
|
||||
pinnedTrackRef?: PinnedTrackRef
|
||||
}
|
||||
|
||||
export const layoutStore = proxy<State>({
|
||||
@@ -20,4 +24,13 @@ export const layoutStore = proxy<State>({
|
||||
activePanelId: null,
|
||||
activeSubPanelId: null,
|
||||
showReactionsToolbar: false,
|
||||
pinnedTrackRef: undefined,
|
||||
})
|
||||
|
||||
export const setPinnedTrack = (trackRef: TrackReferenceOrPlaceholder): void => {
|
||||
layoutStore.pinnedTrackRef = ref(trackRef)
|
||||
}
|
||||
|
||||
export const clearPinnedTrack = (): void => {
|
||||
layoutStore.pinnedTrackRef = undefined
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user