mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-31 20:58:03 +00:00
🚚(frontend) extract ParticipantTile sub-components into their own files
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.
This commit is contained in:
@@ -18,7 +18,7 @@ import {
|
|||||||
} from '@livekit/components-core'
|
} from '@livekit/components-core'
|
||||||
import { Track } from 'livekit-client'
|
import { Track } from 'livekit-client'
|
||||||
import { ParticipantPlaceholder } from './ParticipantPlaceholder'
|
import { ParticipantPlaceholder } from './ParticipantPlaceholder'
|
||||||
import { ParticipantTileFocus } from './ParticipantTileFocus'
|
import { ParticipantTileFocus } from './participantTileFocus/ParticipantTileFocus'
|
||||||
import { FullScreenShareWarning } from './FullScreenShareWarning'
|
import { FullScreenShareWarning } from './FullScreenShareWarning'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { getShortcutDescriptorById } from '@/features/shortcuts/catalog'
|
import { getShortcutDescriptorById } from '@/features/shortcuts/catalog'
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="primaryTextDark"
|
|
||||||
square
|
|
||||||
tooltip={t('fullScreen')}
|
|
||||||
onPress={() => toggleFullScreen()}
|
|
||||||
>
|
|
||||||
<RiFullscreenLine />
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const FocusButton = ({
|
|
||||||
trackRef,
|
|
||||||
}: {
|
|
||||||
trackRef: TrackReferenceOrPlaceholder
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
|
|
||||||
|
|
||||||
const { pinnedTrackRef } = useSnapshot(layoutStore)
|
|
||||||
const inFocus = isEqualTrackRef(trackRef, pinnedTrackRef)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="primaryTextDark"
|
|
||||||
square
|
|
||||||
tooltip={inFocus ? t('pin.disable') : t('pin.enable')}
|
|
||||||
onPress={() => (inFocus ? clearPinnedTrack() : setPinnedTrack(trackRef))}
|
|
||||||
>
|
|
||||||
{inFocus ? <RiUnpinLine /> : <RiPushpin2Line />}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const EffectsButton = () => {
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
|
|
||||||
const { isEffectsOpen, toggleEffects } = useSidePanel()
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
size={'sm'}
|
|
||||||
variant={'primaryTextDark'}
|
|
||||||
square
|
|
||||||
tooltip={t('effects')}
|
|
||||||
onPress={() => !isEffectsOpen && toggleEffects()}
|
|
||||||
>
|
|
||||||
<RiImageCircleAiFill />
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
isDisabled={isMuted}
|
|
||||||
size={'sm'}
|
|
||||||
variant={'primaryTextDark'}
|
|
||||||
square
|
|
||||||
onPress={() => setIsAlertOpen(true)}
|
|
||||||
tooltip={t('muteParticipant', { name })}
|
|
||||||
>
|
|
||||||
{!isMuted ? <RiMicLine /> : <RiMicOffLine />}
|
|
||||||
</Button>
|
|
||||||
<MuteAlertDialog
|
|
||||||
isOpen={isAlertOpen}
|
|
||||||
onSubmit={() =>
|
|
||||||
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<number | null>(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 (
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
position: 'absolute',
|
|
||||||
left: '0',
|
|
||||||
top: '0',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
})}
|
|
||||||
aria-hidden={!isVisible}
|
|
||||||
onMouseEnter={() => setHovered(true)}
|
|
||||||
onMouseLeave={() => setHovered(false)}
|
|
||||||
onMouseMove={handleMouseMove}
|
|
||||||
>
|
|
||||||
{isVisible && (
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
backgroundColor: 'primaryDark.50',
|
|
||||||
transition: 'opacity 200ms linear',
|
|
||||||
zIndex: 1,
|
|
||||||
borderRadius: '0.25rem',
|
|
||||||
display: 'flex',
|
|
||||||
_hover: {
|
|
||||||
opacity: '0.95 !important',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
style={{ opacity }}
|
|
||||||
>
|
|
||||||
<HStack
|
|
||||||
gap={0.5}
|
|
||||||
className={css({
|
|
||||||
padding: '0.5rem',
|
|
||||||
_hover: {
|
|
||||||
opacity: '1 !important',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<FocusButton trackRef={trackRef} />
|
|
||||||
{!isScreenShare ? (
|
|
||||||
<>
|
|
||||||
{participant.isLocal ? (
|
|
||||||
<EffectsButton />
|
|
||||||
) : (
|
|
||||||
canMute && <MuteButton participant={participant} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
!isLocal && <ZoomButton trackRef={trackRef} />
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+20
@@ -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 (
|
||||||
|
<Button
|
||||||
|
size={'sm'}
|
||||||
|
variant={'primaryTextDark'}
|
||||||
|
square
|
||||||
|
tooltip={t('effects')}
|
||||||
|
onPress={() => !isEffectsOpen && toggleEffects()}
|
||||||
|
>
|
||||||
|
<RiImageCircleAiFill />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
+32
@@ -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 (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="primaryTextDark"
|
||||||
|
square
|
||||||
|
tooltip={inFocus ? t('pin.disable') : t('pin.enable')}
|
||||||
|
onPress={() => (inFocus ? clearPinnedTrack() : setPinnedTrack(trackRef))}
|
||||||
|
>
|
||||||
|
{inFocus ? <RiUnpinLine /> : <RiPushpin2Line />}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
+45
@@ -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 (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
isDisabled={isMuted}
|
||||||
|
size={'sm'}
|
||||||
|
variant={'primaryTextDark'}
|
||||||
|
square
|
||||||
|
onPress={() => setIsAlertOpen(true)}
|
||||||
|
tooltip={t('muteParticipant', { name })}
|
||||||
|
>
|
||||||
|
{!isMuted ? <RiMicLine /> : <RiMicOffLine />}
|
||||||
|
</Button>
|
||||||
|
<MuteAlertDialog
|
||||||
|
isOpen={isAlertOpen}
|
||||||
|
onSubmit={() =>
|
||||||
|
muteParticipant(participant).then(() => setIsAlertOpen(false))
|
||||||
|
}
|
||||||
|
onClose={() => setIsAlertOpen(false)}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
+114
@@ -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<number | null>(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 (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
position: 'absolute',
|
||||||
|
left: '0',
|
||||||
|
top: '0',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
})}
|
||||||
|
aria-hidden={!isVisible}
|
||||||
|
onMouseEnter={() => setHovered(true)}
|
||||||
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
>
|
||||||
|
{isVisible && (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
backgroundColor: 'primaryDark.50',
|
||||||
|
transition: 'opacity 200ms linear',
|
||||||
|
zIndex: 1,
|
||||||
|
borderRadius: '0.25rem',
|
||||||
|
display: 'flex',
|
||||||
|
_hover: {
|
||||||
|
opacity: '0.95 !important',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
style={{ opacity }}
|
||||||
|
>
|
||||||
|
<HStack
|
||||||
|
gap={0.5}
|
||||||
|
className={css({
|
||||||
|
padding: '0.5rem',
|
||||||
|
_hover: {
|
||||||
|
opacity: '1 !important',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<FocusButton trackRef={trackRef} />
|
||||||
|
{!isScreenShare ? (
|
||||||
|
<>
|
||||||
|
{participant.isLocal ? (
|
||||||
|
<EffectsButton />
|
||||||
|
) : (
|
||||||
|
canMute && <MuteButton participant={participant} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
!isLocal && <ZoomButton trackRef={trackRef} />
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+32
@@ -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 (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="primaryTextDark"
|
||||||
|
square
|
||||||
|
tooltip={t('fullScreen')}
|
||||||
|
onPress={() => toggleFullScreen()}
|
||||||
|
>
|
||||||
|
<RiFullscreenLine />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user