diff --git a/src/frontend/src/features/layout/components/StageLayout.tsx b/src/frontend/src/features/layout/components/StageLayout.tsx index c3c55ac3..7938598e 100644 --- a/src/frontend/src/features/layout/components/StageLayout.tsx +++ b/src/frontend/src/features/layout/components/StageLayout.tsx @@ -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(null) const lastPinnedParticipantIdentityRef = useRef(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 ? (
@@ -195,10 +181,10 @@ export const StageLayout = () => { > - {focusTrack && } + {pinnedTrackRef && }
)} ) -} \ No newline at end of file +} diff --git a/src/frontend/src/features/notifications/components/ToastJoined.tsx b/src/frontend/src/features/notifications/components/ToastJoined.tsx index b2e2af84..0a9f7553 100644 --- a/src/frontend/src/features/notifications/components/ToastJoined.tsx +++ b/src/frontend/src/features/notifications/components/ToastJoined.tsx @@ -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) { state, ref ) - const layoutContext = useMaybeLayoutContext() const participant = props.toast.content.participant if (!participant) return @@ -38,18 +37,13 @@ export function ToastJoined({ state, ...props }: Readonly) { publication: participant.getTrackPublication(Source.Camera), source: Source.Camera, } - const pinParticipant = () => { - layoutContext?.pin.dispatch?.({ - msg: 'set_pin', - trackReference, - }) - } + return ( { - pinParticipant() + setPinnedTrack(trackReference) closeButtonProps.onPress?.(e) }} > diff --git a/src/frontend/src/features/rooms/livekit/components/ParticipantMenu/PinMenuItem.tsx b/src/frontend/src/features/rooms/livekit/components/ParticipantMenu/PinMenuItem.tsx index feffd28b..a5380b7b 100644 --- a/src/frontend/src/features/rooms/livekit/components/ParticipantMenu/PinMenuItem.tsx +++ b/src/frontend/src/features/rooms/livekit/components/ParticipantMenu/PinMenuItem.tsx @@ -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 ( { name: participant.name, })} className={menuRecipe({ icon: true }).item} - onAction={toggle} + onAction={() => (inFocus ? clearPinnedTrack() : setPinnedTrack(trackRef))} > {inFocus ? ( diff --git a/src/frontend/src/features/rooms/livekit/components/ParticipantTile.tsx b/src/frontend/src/features/rooms/livekit/components/ParticipantTile.tsx index e1405d2a..66c2a0a4 100644 --- a/src/frontend/src/features/rooms/livekit/components/ParticipantTile.tsx +++ b/src/frontend/src/features/rooms/livekit/components/ParticipantTile.tsx @@ -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({ @@ -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({ diff --git a/src/frontend/src/features/rooms/livekit/components/ParticipantTileFocus.tsx b/src/frontend/src/features/rooms/livekit/components/ParticipantTileFocus.tsx index 8caf64ab..89c89973 100644 --- a/src/frontend/src/features/rooms/livekit/components/ParticipantTileFocus.tsx +++ b/src/frontend/src/features/rooms/livekit/components/ParticipantTileFocus.tsx @@ -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 ( diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/PinBadge.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/PinBadge.tsx index 5aec0887..d56400eb 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Participants/PinBadge.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/PinBadge.tsx @@ -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 diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index 860f94c6..4b0dfd87 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -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 {isWeb() && ( - + <> setIsShareErrorVisible(false)} /> - {isPictureInPictureOpen ? ( - - ) : ( - - )} + {isPictureInPictureOpen ? : } { @@ -119,7 +104,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) { }} /> - + )} diff --git a/src/frontend/src/stores/layout.ts b/src/frontend/src/stores/layout.ts index 4b30144f..74bb52e9 100644 --- a/src/frontend/src/stores/layout.ts +++ b/src/frontend/src/stores/layout.ts @@ -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> type State = { showHeader: boolean @@ -11,6 +14,7 @@ type State = { activePanelId: PanelId | null activeSubPanelId: SubPanelId | null showReactionsToolbar: boolean + pinnedTrackRef?: PinnedTrackRef } export const layoutStore = proxy({ @@ -20,4 +24,13 @@ export const layoutStore = proxy({ 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 +}