From a3851842e9e9f69a311e90ef7ec33b8ad299ce3d Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 23 Jul 2026 19:58:30 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=9A(frontend)=20extract=20ParticipantT?= =?UTF-8?q?ile=20sub-components=20into=20their=20own=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the sub-components currently declared inside ParticipantTile into dedicated files. This makes the ParticipantTile file easier to read and lets each sub-component be imported and reasoned about on its own. --- .../components/ParticipantTile.tsx | 2 +- .../components/ParticipantTileFocus.tsx | 233 ------------------ .../participantTileFocus/EffectsButton.tsx | 20 ++ .../participantTileFocus/FocusButton.tsx | 32 +++ .../participantTileFocus/MuteButton.tsx | 45 ++++ .../ParticipantTileFocus.tsx | 114 +++++++++ .../participantTileFocus/ZoomButton.tsx | 32 +++ 7 files changed, 244 insertions(+), 234 deletions(-) delete mode 100644 src/frontend/src/features/participantTile/components/ParticipantTileFocus.tsx create mode 100644 src/frontend/src/features/participantTile/components/participantTileFocus/EffectsButton.tsx create mode 100644 src/frontend/src/features/participantTile/components/participantTileFocus/FocusButton.tsx create mode 100644 src/frontend/src/features/participantTile/components/participantTileFocus/MuteButton.tsx create mode 100644 src/frontend/src/features/participantTile/components/participantTileFocus/ParticipantTileFocus.tsx create mode 100644 src/frontend/src/features/participantTile/components/participantTileFocus/ZoomButton.tsx diff --git a/src/frontend/src/features/participantTile/components/ParticipantTile.tsx b/src/frontend/src/features/participantTile/components/ParticipantTile.tsx index ae2f0cd4..2d754507 100644 --- a/src/frontend/src/features/participantTile/components/ParticipantTile.tsx +++ b/src/frontend/src/features/participantTile/components/ParticipantTile.tsx @@ -18,7 +18,7 @@ import { } from '@livekit/components-core' import { Track } from 'livekit-client' import { ParticipantPlaceholder } from './ParticipantPlaceholder' -import { ParticipantTileFocus } from './ParticipantTileFocus' +import { ParticipantTileFocus } from './participantTileFocus/ParticipantTileFocus' import { FullScreenShareWarning } from './FullScreenShareWarning' import { useTranslation } from 'react-i18next' import { getShortcutDescriptorById } from '@/features/shortcuts/catalog' diff --git a/src/frontend/src/features/participantTile/components/ParticipantTileFocus.tsx b/src/frontend/src/features/participantTile/components/ParticipantTileFocus.tsx deleted file mode 100644 index fe733c64..00000000 --- a/src/frontend/src/features/participantTile/components/ParticipantTileFocus.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { css } from '@/styled-system/css' -import { HStack } from '@/styled-system/jsx' -import { Button } from '@/primitives' -import { - RiFullscreenLine, - RiImageCircleAiFill, - RiMicLine, - RiMicOffLine, - RiPushpin2Line, - RiUnpinLine, -} from '@remixicon/react' -import { useTrackMutedIndicator } from '@livekit/components-react' -import { useTranslation } from 'react-i18next' -import { - isEqualTrackRef, - TrackReferenceOrPlaceholder, -} from '@livekit/components-core' -import { useEffect, useRef, useState } from 'react' -import { type Participant, Track } from 'livekit-client' -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' -import { useFullScreen } from '@/features/rooms/livekit/hooks/useFullScreen' -import { MuteAlertDialog } from '@/features/rooms/livekit/components/MuteAlertDialog' -import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel' - -const ZoomButton = ({ - trackRef, -}: { - trackRef: TrackReferenceOrPlaceholder -}) => { - const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' }) - const { toggleFullScreen, isFullscreenAvailable } = useFullScreen({ - trackRef, - }) - - if (!isFullscreenAvailable) { - return - } - - return ( - - ) -} - -const FocusButton = ({ - trackRef, -}: { - trackRef: TrackReferenceOrPlaceholder -}) => { - const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' }) - - const { pinnedTrackRef } = useSnapshot(layoutStore) - const inFocus = isEqualTrackRef(trackRef, pinnedTrackRef) - - return ( - - ) -} - -const EffectsButton = () => { - const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' }) - const { isEffectsOpen, toggleEffects } = useSidePanel() - return ( - - ) -} - -const MuteButton = ({ participant }: { participant: Participant }) => { - const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' }) - - const { isMuted } = useTrackMutedIndicator({ - participant: participant, - source: Track.Source.Microphone, - }) - - const { muteParticipant } = useMuteParticipant() - const [isAlertOpen, setIsAlertOpen] = useState(false) - - const name = participant.name || participant.identity - - return ( - <> - - - muteParticipant(participant).then(() => setIsAlertOpen(false)) - } - onClose={() => setIsAlertOpen(false)} - name={name} - /> - - ) -} - -const MOUSE_IDLE_TIME = 3000 - -export const ParticipantTileFocus = ({ - trackRef, - hasKeyboardFocus, -}: { - trackRef: TrackReferenceOrPlaceholder - hasKeyboardFocus: boolean -}) => { - const [hovered, setHovered] = useState(false) - const [opacity, setOpacity] = useState(0) - - const idleTimerRef = useRef(null) - const [isIdleRef, setIsIdleRef] = useState(false) - - const isVisible = hasKeyboardFocus || (hovered && !isIdleRef) - - useEffect(() => { - if (isVisible) { - // Wait for next frame to ensure element is mounted - requestAnimationFrame(() => { - setOpacity(0.6) - }) - } else { - setOpacity(0) - } - }, [isVisible]) - - const handleMouseMove = () => { - if (idleTimerRef.current) { - window.clearTimeout(idleTimerRef.current) - } - idleTimerRef.current = window.setTimeout(() => { - setIsIdleRef(true) - }, MOUSE_IDLE_TIME) - setIsIdleRef(false) - } - - const participant = trackRef.participant - - const isScreenShare = trackRef.source == Track.Source.ScreenShare - const isLocal = trackRef.participant.isLocal - - const canMute = useCanMute(participant) - - return ( -
setHovered(true)} - onMouseLeave={() => setHovered(false)} - onMouseMove={handleMouseMove} - > - {isVisible && ( -
- - - {!isScreenShare ? ( - <> - {participant.isLocal ? ( - - ) : ( - canMute && - )} - - ) : ( - !isLocal && - )} - -
- )} -
- ) -} diff --git a/src/frontend/src/features/participantTile/components/participantTileFocus/EffectsButton.tsx b/src/frontend/src/features/participantTile/components/participantTileFocus/EffectsButton.tsx new file mode 100644 index 00000000..7060ec2b --- /dev/null +++ b/src/frontend/src/features/participantTile/components/participantTileFocus/EffectsButton.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from 'react-i18next' +import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel' +import { Button } from '@/primitives' +import { RiImageCircleAiFill } from '@remixicon/react' + +export const EffectsButton = () => { + const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' }) + const { isEffectsOpen, toggleEffects } = useSidePanel() + return ( + + ) +} diff --git a/src/frontend/src/features/participantTile/components/participantTileFocus/FocusButton.tsx b/src/frontend/src/features/participantTile/components/participantTileFocus/FocusButton.tsx new file mode 100644 index 00000000..e31c9ef4 --- /dev/null +++ b/src/frontend/src/features/participantTile/components/participantTileFocus/FocusButton.tsx @@ -0,0 +1,32 @@ +import { + isEqualTrackRef, + TrackReferenceOrPlaceholder, +} from '@livekit/components-core' +import { useTranslation } from 'react-i18next' +import { useSnapshot } from 'valtio' +import { clearPinnedTrack, layoutStore, setPinnedTrack } from '@/stores/layout' +import { Button } from '@/primitives' +import { RiPushpin2Line, RiUnpinLine } from '@remixicon/react' + +export const FocusButton = ({ + trackRef, +}: { + trackRef: TrackReferenceOrPlaceholder +}) => { + const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' }) + + const { pinnedTrackRef } = useSnapshot(layoutStore) + const inFocus = isEqualTrackRef(trackRef, pinnedTrackRef) + + return ( + + ) +} diff --git a/src/frontend/src/features/participantTile/components/participantTileFocus/MuteButton.tsx b/src/frontend/src/features/participantTile/components/participantTileFocus/MuteButton.tsx new file mode 100644 index 00000000..97a7ab12 --- /dev/null +++ b/src/frontend/src/features/participantTile/components/participantTileFocus/MuteButton.tsx @@ -0,0 +1,45 @@ +import { Participant, Track } from 'livekit-client' +import { useTranslation } from 'react-i18next' +import { useTrackMutedIndicator } from '@livekit/components-react' +import { useMuteParticipant } from '@/features/rooms/api/muteParticipant' +import { useState } from 'react' +import { Button } from '@/primitives' +import { RiMicLine, RiMicOffLine } from '@remixicon/react' +import { MuteAlertDialog } from '@/features/rooms/livekit/components/MuteAlertDialog' + +export const MuteButton = ({ participant }: { participant: Participant }) => { + const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' }) + + const { isMuted } = useTrackMutedIndicator({ + participant: participant, + source: Track.Source.Microphone, + }) + + const { muteParticipant } = useMuteParticipant() + const [isAlertOpen, setIsAlertOpen] = useState(false) + + const name = participant.name || participant.identity + + return ( + <> + + + muteParticipant(participant).then(() => setIsAlertOpen(false)) + } + onClose={() => setIsAlertOpen(false)} + name={name} + /> + + ) +} diff --git a/src/frontend/src/features/participantTile/components/participantTileFocus/ParticipantTileFocus.tsx b/src/frontend/src/features/participantTile/components/participantTileFocus/ParticipantTileFocus.tsx new file mode 100644 index 00000000..66c4e762 --- /dev/null +++ b/src/frontend/src/features/participantTile/components/participantTileFocus/ParticipantTileFocus.tsx @@ -0,0 +1,114 @@ +import { css } from '@/styled-system/css' +import { HStack } from '@/styled-system/jsx' +import { TrackReferenceOrPlaceholder } from '@livekit/components-core' +import { useEffect, useRef, useState } from 'react' +import { Track } from 'livekit-client' +import { useCanMute } from '@/features/rooms/livekit/hooks/useCanMute' +import { FocusButton } from './FocusButton' +import { EffectsButton } from './EffectsButton' +import { MuteButton } from './MuteButton' +import { ZoomButton } from './ZoomButton' + +const MOUSE_IDLE_TIME = 3000 + +export const ParticipantTileFocus = ({ + trackRef, + hasKeyboardFocus, +}: { + trackRef: TrackReferenceOrPlaceholder + hasKeyboardFocus: boolean +}) => { + const [hovered, setHovered] = useState(false) + const [opacity, setOpacity] = useState(0) + + const idleTimerRef = useRef(null) + const [isIdleRef, setIsIdleRef] = useState(false) + + const isVisible = hasKeyboardFocus || (hovered && !isIdleRef) + + useEffect(() => { + if (isVisible) { + // Wait for next frame to ensure element is mounted + requestAnimationFrame(() => { + setOpacity(0.6) + }) + } else { + setOpacity(0) + } + }, [isVisible]) + + const handleMouseMove = () => { + if (idleTimerRef.current) { + window.clearTimeout(idleTimerRef.current) + } + idleTimerRef.current = window.setTimeout(() => { + setIsIdleRef(true) + }, MOUSE_IDLE_TIME) + setIsIdleRef(false) + } + + const participant = trackRef.participant + + const isScreenShare = trackRef.source == Track.Source.ScreenShare + const isLocal = trackRef.participant.isLocal + + const canMute = useCanMute(participant) + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + onMouseMove={handleMouseMove} + > + {isVisible && ( +
+ + + {!isScreenShare ? ( + <> + {participant.isLocal ? ( + + ) : ( + canMute && + )} + + ) : ( + !isLocal && + )} + +
+ )} +
+ ) +} diff --git a/src/frontend/src/features/participantTile/components/participantTileFocus/ZoomButton.tsx b/src/frontend/src/features/participantTile/components/participantTileFocus/ZoomButton.tsx new file mode 100644 index 00000000..4e637c08 --- /dev/null +++ b/src/frontend/src/features/participantTile/components/participantTileFocus/ZoomButton.tsx @@ -0,0 +1,32 @@ +import { TrackReferenceOrPlaceholder } from '@livekit/components-core' +import { useTranslation } from 'react-i18next' +import { useFullScreen } from '@/features/rooms/livekit/hooks/useFullScreen' +import { Button } from '@/primitives' +import { RiFullscreenLine } from '@remixicon/react' + +export const ZoomButton = ({ + trackRef, +}: { + trackRef: TrackReferenceOrPlaceholder +}) => { + const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' }) + const { toggleFullScreen, isFullscreenAvailable } = useFullScreen({ + trackRef, + }) + + if (!isFullscreenAvailable) { + return + } + + return ( + + ) +}